2025, Oct 19 03:31

Python जेनेरिक में Union bound: टाइप चेकर समस्याएँ और समाधान

Python 3.13 में Union bound वाले जेनेरिक पर Self आधारित merge क्यों विफल होता है, Pylance/Pyright क्या चेताते हैं, और constrained generics से सुरक्षित समाधान पाएं।

जब किसी जेनेरिक क्लास को यूनियन से पैरामीटराइज़ किया जाता है, तो जो टाइप संबंध पहली नज़र में साफ दिखते हैं, वे टाइप चेकर के सामने अक्सर बिखर जाते हैं। एक आम स्थिति वह होती है जब किसी मेथड में self और other दोनों एक ही जेनेरिक इंस्टैंशिएशन होते हैं, फिर भी उसी मेथड के अंदर किसी फ़ील्ड का एक्सेस टाइप चेकिंग में फेल हो जाता है। नीचे Python 3.13.3 में Pylance के साथ यही स्थिति दिखाने के लिए एक न्यूनतम उदाहरण दिया है।

समस्या सेटअप

from typing import Self
class PartA:
    m: int
    def merge(self, peer: Self) -> None:
        self.m += peer.m
class PartB:
    s: str
    def merge(self, peer: Self) -> None:
        self.s += peer.s
PartMix = PartA | PartB
class Holder[PieceT: PartMix]:
    piece: PieceT
    def join(self, peer_box: Self) -> None:
        self.piece.merge(peer_box.piece)  # Pylance इस पंक्ति पर चेतावनी देता है

Pylance बताता है कि peer_box.piece का आर्गुमेंट प्रकार merge के लिए बहुत व्यापक है। संदेश कुछ इस तरह दिखता है:

"Argument of type \"PieceT@Holder\" cannot be assigned to parameter \"peer\" of type \"PartA*\" in function \"merge\". Type \"PartA* | PartB*\" is not assignable to type \"PartA*\". \"PartB*\" is not assignable to \"PartA*\". Similarly for \"PartB*\"."

यह क्यों होता है

join के भीतर, self और peer_box दोनों ही एक जैसे Holder[PieceT] हैं। सहज रूप से आप उम्मीद करेंगे कि self.piece और peer_box.piece एक ही ठोस प्रकार साझा करें। लेकिन क्योंकि PieceT एक यूनियन से बाउंड है, Pylance peer_box.piece को PartMix ही मानता है और कॉल साइट पर merge को चाहिए वाले विशिष्ट ब्रांच से उसका मेल साबित नहीं कर पाता।

और भी अहम बात यह है कि टाइप पैरामीटर की upper bound के रूप में यूनियन की अनुमति देने से स्वयं जेनेरिक का एक असुरक्षित इंस्टैंशिएशन संभव हो जाता है। इस परिभाषा के तहत Holder[PartMix] वैध है, और यही अपेक्षित कॉन्ट्रैक्ट तोड़ देता है। नीचे का क्रम देखें—यह टाइप चेक पास कर जाता है, लेकिन रनटाइम पर विफल होता है क्योंकि वास्तविक pieces आपस में मेल नहीं खाते:

mix_a: PartMix = PartA()
box_a: Holder[PartMix] = Holder(mix_a)
mix_b: PartMix = PartB()
box_b: Holder[PartMix] = Holder(mix_b)
box_a.join(box_b)  # दोनों Holder[PartMix] हैं, लेकिन रनटाइम पर घटक असंगत हैं

यह दिखाता है कि टाइप चेकर merge कॉल को स्वीकार क्यों नहीं करता: उसे ऐसी स्थिति भी स्वीकार करनी पड़ती जहाँ सुरक्षा सुनिश्चित करना संभव नहीं है। यहाँ typing.cast का इस्तेमाल भी मदद नहीं करता, क्योंकि मूल समस्या वही उदार bound है जो Holder[PartMix] जैसे इंस्टैंशिएशन को अनुमति देता है।

(यह मानते हुए कि piece एक इंस्टेंस एट्रिब्यूट है जिसे कंस्ट्रक्टर के माध्यम से सेट किया जाता है, न कि क्लास एट्रिब्यूट।)

सीमाबद्ध जेनेरिक्स के साथ सुरक्षित समाधान

यूनियन को upper bound बनाने के बजाय, टाइप पैरामीटर को निश्चित विकल्पों के सेट तक सीमाबद्ध करें। इससे अपेक्षित लचीलेपन बना रहता है—Holder को PartA या PartB के साथ specialize किया जा सकता है—लेकिन असुरक्षित Holder[PartA | PartB] इंस्टैंशिएशन रुक जाता है।

class Holder[PieceT: (PartA, PartB)]:
    piece: PieceT
    def join(self, peer_box: Self) -> None:
        self.piece.merge(peer_box.piece)

इस परिभाषा में Holder[PartA] और Holder[PartB] दोनों मान्य हैं, पर Holder[PartA | PartB] नहीं। अब join कॉल टाइप चेक पास करती है और रनटाइम व्यवहार से मेल खाती है। यह Pyright और mypy—दोनों में—पास होता है।

यह क्यों मायने रखता है

जेनेरिक्स में यूनियन-टाइप bounds सुविधाजनक लग सकती हैं, लेकिन वे अक्सर अस्पष्टता को जेनेरिक में ही समाहित कर देती हैं। यह अस्पष्टता Self-जैसे रिश्तों पर निर्भर मेथड्स तक रिसती है और मेंबर्स को यूनियन ही बनाए रखती है, जिससे वैध कॉल्स को सुरक्षित साबित करना संभव नहीं रह जाता। इससे भी बुरा, ऐसे जेनेरिक इंस्टैंशिएशन वैध हो जाते हैं जो असाउंड हैं और रनटाइम पर क्रैश कर सकते हैं। टाइप पैरामीटर को स्पष्ट विकल्पों के सेट तक सीमित करना इस अस्पष्टता को हटाता है, अभिव्यक्तिता से समझौता किए बिना।

निष्कर्ष

यदि किसी जेनेरिक को कई ठोस इम्प्लीमेंटेशन का समर्थन करना है, तो टाइप पैरामीटर की bound के रूप में यूनियन का उपयोग न करें। टाइप पैरामीटर पर constraints लगाएँ ताकि जेनेरिक हर बार केवल एक संगत विकल्प के साथ ही इंस्टैंशिएट हो। इससे मेथड बॉडीज़ टाइप-सुरक्षित रहती हैं, आकस्मिक Holder[PartA | PartB] इंस्टैंशिएशन रुकते हैं, और चेकर की गारंटियाँ रनटाइम व्यवहार से मेल खाती हैं।

यह लेख StackOverflow के प्रश्न (लेखक: inaku Gyan) और Anerdw के उत्तर पर आधारित है।