2025, Sep 28 13:31
list बनाम Sequence: Python टाइप चेकर, inference और flow
Python में list की invariance और Sequence की covariance को उदाहरणों से समझें। जानें कैसे bidirectional inference व flow analysis append/extend के नियमों को प्रभावित करते हैं.
टाइप चेकर कड़ाई से काम करते हैं, और कई बार यह कड़ाई उलटी-पुलटी लग सकती है। एक क्लासिक उदाहरण है सूचियों (list) में उप-टाइप्स को मिलाना बनाम Sequence जैसी सह-परिवर्ती (covariant) व्यूज़ के साथ काम करना। नीचे इस उलझन की जड़ और यह कि द्विदिश अनुमान (bidirectional inference) व फ्लो विश्लेषण (flow analysis) परिणाम को कैसे प्रभावित करते हैं—का चरण-दर-चरण विवरण है।
स्थिति पुनरुत्पादन
from typing import Sequence
class BaseItem: ...
class SubA(BaseItem): ...
class SubB(BaseItem): ...
items: list[BaseItem] = [SubA()]
items.append(SubB())  # ठीक है: BaseItem की सूची में SubA और SubB दोनों रखे जा सकते हैं
अब तक सब ठीक। अब एक ऐसी फ़ंक्शन अलग करें जो किसी विशेष उप-टाइप की list लौटाए, और उसे बेस टाइप की list में असाइन करके देखें।
def build_suba_list() -> list[SubA]:
    return [SubA(), SubA()]
items2: list[BaseItem] = build_suba_list()  # list की invariance (अपरिवर्तनशीलता) के कारण त्रुटि
यह त्रुटि वाजिब है क्योंकि list invariant होती है। लेकिन Sequence जैसी covariant व्यू आकर्षक लगती है:
more_items: Sequence[BaseItem] = build_suba_list()
more_items.append(SubB())  # टाइप चेकर त्रुटि
यहाँ append क्यों विफल होता है, और यह वैरिएशन क्यों पास हो जाता है?
extra_items: Sequence[BaseItem] = []
extra_items.extend(build_suba_list())  # ठीक है
extra_items.append(SubB())              # ठीक है
असल में क्या हो रहा है
यहाँ दो फीचर काम कर रहे हैं: bidirectional inference और flow analysis। इन्हीं से शुरुआत से अंत तक का व्यवहार समझाया जा सकता है।
Bidirectional inference अपेक्षित टाइप का सहारा लेकर सामान्य अनुमान को स्पष्ट करता है। यह छोटा उदाहरण देखें:
from typing import Iterable, Literal
xs = [3]
# अपने आप में, यह कई तरह का हो सकता है: list[int], Iterable[object], आदि।
eys: Iterable[Literal[3]] = [3]
reveal_type(ys)  # अपेक्षित Iterable[Literal[3]] के कारण list[Literal[3]]
Flow analysis आवश्यकता पड़ने पर किसी प्रतीक (symbol) को उसके घोषित/अनुमानित टाइप से अधिक सटीक टाइप तक संकीर्ण कर सकता है, बशर्ते यह sound हो:
num: int = 3
reveal_type(num)  # Literal[3], int से संकीर्ण
res = num + 2
reveal_type(res)  # Literal[5], केवल int नहीं
वापस list बनाम Sequence की पहेली पर
किसी फ़ंक्शन का रिटर्न मान, जो एक ठोस list उप-टाइप देता है, जब Sequence के रूप में एनोटेटेड नाम को असाइन किया जाता है, तो चेकर केवल एनोटेटेड व्यू नहीं, बल्कि वास्तविक मान के लिए एक सटीक टाइप रखता है। यहाँ flow analysis सक्रिय रहता है और अभिव्यक्ति का वास्तविक टाइप बनाए रखता है।
from typing import Sequence
class BaseItem: ...
class SubA(BaseItem): ...
class SubB(BaseItem): ...
def build_suba_list() -> list[SubA]:
    return [SubA(), SubA()]
view1: Sequence[BaseItem] = build_suba_list()
reveal_type(view1)  # दाएँ भाग के flow analysis से list[SubA]
# इसलिए मेथड रेज़ोल्यूशन list[SubA].append को लक्ष्य करता है,
# जो SubB स्वीकार नहीं कर सकता।
view1.append(SubB())  # त्रुटि: list[SubA].append के साथ संगत नहीं
इसके विपरीत, जहाँ अपेक्षित टाइप Sequence[BaseItem] है वहाँ खाली list बनाना अनुमान को अलग दिशा देता है। खाली लिटरल, अपेक्षित टाइप और बाद की mutations मिलकर चेकर को अधोस्तरीय list को list[BaseItem] मानने के लिए प्रेरित करते हैं, और फिर व्यवहार अपेक्षा के अनुरूप होता है।
view2: Sequence[BaseItem] = []
reveal_type(view2)  # अपेक्षित टाइप और flow analysis के कारण list[BaseItem]
# Iterable[SubA] से list[BaseItem] को बढ़ाना ठीक है,
# क्योंकि list[SubA], Iterable[BaseItem] का उप-टाइप है।
view2.extend(build_suba_list())  # ठीक है
# list[BaseItem] में SubB जोड़ना भी मान्य है।
view2.append(SubB())  # ठीक है
यह फर्क सूक्ष्म क्यों है, और महत्वपूर्ण भी
जब आप किसी नाम से कोई मान बाँधते हैं, चेकर उस मान के लिए उपलब्ध सबसे सटीक टाइप साथ रखता है। यदि दाईं ओर list[SubA] है, तो मेथड रेज़ोल्यूशन के दौरान वह नाम कारगर रूप से list[SubA] माना जाता है, चाहे एनोटेशन में Sequence[BaseItem] लिखा हो। इसी कारण पहले मामले में append विफल होता है: अधोस्तरीय कंटेनर अब भी list[SubA] के रूप में विश्लेषित हो रहा है, और SubB उसमें फिट नहीं बैठता।
जब आप खाली list और अपेक्षित बेस व्यू के साथ कंटेनर को वहीं बनाते हैं, चेकर विकसित कंटेनर को list[BaseItem] मानता है, और बाद की सभी mutations उसी लक्ष्य टाइप के विरुद्ध जाँची जाती हैं। list[SubA] से extend करना चलता है, क्योंकि list[BaseItem] को बढ़ाने के लिए Iterable[BaseItem] चाहिए होता है, और list[SubA] इसके अनुरूप है। SubB को append करना भी मान्य है, क्योंकि वह BaseItem का उप-टाइप है।
व्यावहारिक समाधान
यदि आपको covariant व्यू चाहिए, पर साथ ही बेस टाइप के अनुरूप mutations भी करनी हैं, तो पहले उसी संदर्भ में कंटेनर को इनिशियलाइज़ करें जो अपेक्षित बेस टाइप तय करता है, फिर उसे भरें। किसी तैयार list[SubA] को सीधे Sequence[BaseItem] नाम पर असाइन करने से प्रभावी टाइप list[SubA] तक संकीर्ण हो जाता है, और मेथड रेज़ोल्यूशन उसी संकीर्ण टाइप का अनुसरण करता है—नतीजतन अन्य उप-टाइप्स को append करना रुक जाता है।
सारांश
यह व्यवहार सीधे-सीधे bidirectional inference और flow analysis से निकलता है। चेकर वास्तविक मानों के बारे में सटीक जानकारी कैसे सँभालता है, यह समझने से साफ हो जाता है कि तैयार list के साथ Sequence अपने-आप मेथड टाइप्स को ढीला नहीं करता, और क्यों अपेक्षित बेस टाइप के संदर्भ में कंटेनर बनाना मनचाहा परिणाम देता है। विकास के दौरान reveal_type का उपयोग करें ताकि हर चरण पर चेकर ने क्या निष्कर्ष निकाला है, यह पक्का हो सके और अप्रत्याशित चीज़ों से बचा जा सके।