2025, Sep 27 17:33
Python 3.13 में जेनेरिक variance के लिए सही top materialization
Python 3.13 में जेनेरिक variance संभालने का तरीका जानें: Any, Never और object के साथ सही top materialization चुनें, basedpyright में चेतावनियों से बचें.
किसी जेनेरिक क्लास के किसी भी इंस्टेंस को स्वीकार करने वाले मेथड की टाइपिंग पहली नज़र में आसान लग सकती है, लेकिन जैसे ही variance तस्वीर में आता है, बात उलझ जाती है। Python 3.13 में नई जेनेरिक सिंटैक्स के साथ “कोई भी Foo[T] चलेगा, T कैसा है इसकी परवाह नहीं” जैसा सरल विचार टाइप-चेकर की अपेक्षाओं से टकरा जाता है, खासकर basedpyright जैसे टूल्स में। सवाल यह है कि इस मंशा को कैसे व्यक्त किया जाए—बिना हर जगह Any फैलाए या फ़ंक्शन सिग्नेचर को ज़रूरत से ज़्यादा पैरामीटराइज़ किए।
समस्या की रूपरेखा
मान लीजिए हमारे पास एक जेनेरिक क्लास है, और किसी खास फ़ंक्शन में उसके टाइप पैरामीटर की आवश्यकता नहीं पड़ती। क्लास खुद तो सामान्य है; असल रगड़ कॉल-बाउंड्री पर उभरती है।
class Crate[U]: ...
def handle_crate_a(x: Crate) -> None: ...
from typing import Any
def handle_crate_b(x: Crate[Any]) -> None: ...
def handle_crate_c[U](x: Crate[U]) -> None: ...
पहले प्रयास में ब्रैकेट्स को पूरी तरह छोड़ दिया गया है, और basedpyright इसमें त्रुटि दिखाता है। दूसरे में Any आयात कर लिया जाता है, जिस पर एक चेतावनी मिलती है—कई डेवलपर्स इसे इनलाइन या ग्लोबली दबाने की कोशिश करते हैं, पर यह उस चेतावनी के उद्देश्य को ही निष्फल कर देता है। तीसरे में टाइप पैरामीटर दोहराने पड़ते हैं और अगर U पर कोई upper bound है तो उसे सिग्नेचर में फिर से लिखना पड़ता है—ऐसे फ़ंक्शन के लिए अतिरिक्त coupling और शोर, जो वास्तव में U का उपयोग ही नहीं करता।
असल में हो क्या रहा है
किसी जेनेरिक क्लास का सबसे सामान्य, पूरी तरह स्टैटिक टाइप उसके टाइप पैरामीटर्स की variance से तय होता है। ऐसे टाइप को top materialization (या upper bound materialization) कहा जाता है। विचार यह है कि टाइप पैरामीटर के लिए ऐसा आर्ग्युमेंट चुना जाए जो “सबसे व्यापक रूप से सुरक्षित टाइप” को दर्शाए—पर यह चयन इस पर निर्भर करता है कि पैरामीटर covariant, contravariant, bivariant है या invariant।
अगर पैरामीटर covariant है, तो top materialization Crate[object] होगा—या अगर कोई upper bound है तो Crate[UpperBound]। अगर पैरामीटर contravariant है, तो top materialization Crate[Never] होगा। और अगर पैरामीटर invariant है, तो फिलहाल कोई denotable top materialization उपलब्ध नहीं है; व्यवहार में सबसे उपयुक्त विकल्प Crate[Any] ही है।
कोड में समाधान
जेनेरिक पैरामीटर की variance के अनुरूप top materialization का उपयोग करें। पैरामीटर यदि covariant या bivariant है, तो object या उसके upper bound से एनोटेट करें। अगर वह contravariant है, तो Never का प्रयोग करें। और यदि invariant है, तो Any पर fallback करें।
from typing import Any, Never
# कोवेरिएंट या बाइवेरिएंट जेनेरिक पैरामीटर
def handle_crate(x: Crate[object]) -> None: ...
# कंट्रावेरिएंट जेनेरिक पैरामीटर
def handle_crate(x: Crate[Never]) -> None: ...
# इनवेरिएंट जेनेरिक पैरामीटर (फिलहाल कोई बेहतर स्थिर विकल्प नहीं)
def handle_crate(x: Crate[Any]) -> None: ...
यदि टाइप पैरामीटर पर कोई upper bound है और पैरामीटर covariant है, तो object की जगह उसी bound से एनोटेट करें। इससे “किसी भी Crate” का अर्थ उस बाधा के तहत सटीकता से व्यक्त होता है और टाइप पूरी तरह स्टैटिक रहता है।
यह क्यों मायने रखता है
सही top materialization चुनने से सिग्नेचर भंगुर नहीं होते और जहां टाइप पैरामीटर की जरूरत नहीं, वहां अनावश्यक जेनेरिक्स से बचा जा सकता है। साथ ही Any पर निर्भर होने का लोभ भी टलता है। अनुभव बताता है कि Any उस रास्ते पर टाइप-चेकिंग को प्रभावी रूप से निष्क्रिय कर देता है—शुरुआत में यह सुविधाजनक लग सकता है, पर बाद में आप जिन सार्थक डायग्नॉस्टिक्स की उम्मीद करते हैं, उन्हें यह दबा सकता है। basedpyright की reportExplicitAny चेतावनी ठीक इसी समझौते को सामने लाती है।
एक परिशिष्ट: व्यवहार में यह कैसे सुलझता है
कुछ टूलिंग top materializations का निर्धारण स्वतः करती है। ty resolver एक उदाहरण है।
class BothWays[V]: ...
class GoesOut[T]:
    def show(self) -> T: ...
class GoesIn[T]:
    def add(self, _: T) -> None: ...
class Strict[T]:
    def mix(self, _: T) -> T: ...
from typing import Any
from ty_extensions import Top
def probe(
    a: Top[BothWays[Any]],
    b: Top[GoesOut[Any]],
    c: Top[GoesIn[Any]],
    d: Top[Strict[Any]]
):
    reveal_type(a)      # BothWays[object]
    reveal_type(b)      # GoesOut[object]
    reveal_type(c)      # GoesIn[Never]
    reveal_type(d)      # Top[Strict[Any]]
निष्कर्ष और सुझाव
जब आप चाहते हैं कि कोई फ़ंक्शन किसी जेनेरिक क्लास के किसी भी इंस्टेंस को स्वीकार करे—बिना उसके ठोस टाइप पैरामीटर की परवाह किए—तो top materialization पर भरोसा करें। Covariant या bivariant पैरामीटर के लिए object या घोषित upper bound से एनोटेट करें। Contravariant पैरामीटर के लिए Never का उपयोग करें। Invariant पैरामीटर के लिए, वर्तमान में कोई denotable top materialization नहीं है, इसलिए Any चुनें। यह तरीका सिग्नेचर्स को ईमानदार और स्टैटिक रखता है, फालतू जेनेरिक पैरामीटर्स से बचाता है, और यह स्पष्ट करता है कि फ़ंक्शन क्लास की किसी भी वैध इंस्टैंशिएशन के साथ काम करता है।
यदि आप केवल इसलिए किसी पैरामीटर को covariant करने पर विचार कर रहे हैं ताकि एनोटेशन में object या upper bound लिख सकें, तो पहले यह परखें कि क्या आपका API सचमुच covariance को संतुष्ट करता है। अगर हां, तो परिणाम अभिव्यंजक भी होगा और भविष्य-सुरक्षित भी। वरना, सही variance ही चुनें और उसके अनुरूप top materialization अपनाएं—भले ही invariant के लिए इसका मतलब Any के साथ जीना ही क्यों न हो।
यह लेख StackOverflow के प्रश्न (लेखक: Frank William Hammond) और InSync के उत्तर पर आधारित है।