2025, Sep 25 09:31
Python में Optional शॉर्ट‑सर्किटिंग पर mypy की चेतावनी और @final से समाधान
जानें क्यों Python में Optional पर x and x.value() शॉर्ट‑सर्किटिंग mypy में टाइप त्रुटि दिखाती है, और कैसे @final truthiness व subclassing से जुड़े जोखिम हल करती
Python में Optional मानों के साथ शॉर्ट‑सर्किटिंग अक्सर सुघड़ लगती है: एक कॉम्पैक्ट अभिव्यक्ति लिखिए और None पर साफ‑सुथरा फॉलबैक पाइए। लेकिन जैसे ही static typing तस्वीर में आती है, यह सुविधा truthiness और subclassing के बारे में टाइप चेकर के तर्क से टकरा सकती है। यहाँ mypy का एक ठोस उदाहरण है, जो तब तक झूठा अलर्ट लगता है जब तक आप रनटाइम पर क्या हो सकता है, उसे न देखें।
यदि कोई Python क्लास
__bool__या__len__परिभाषित नहीं करती, तोbool(instance)का डिफ़ॉल्ट मानTrueहोता है।
समस्या
आपके पास एक क्लास है जिसमें एक साधारण मेथड है, और एक हेल्पर जो या तो इंस्टेंस लेता है या None। सहज एक‑लाइनर x and x.method() से उम्मीद है कि या तो मेथड का परिणाम लौटे या फिर None। फिर भी mypy असंगत रिटर्न टाइप की रिपोर्ट करता है।
class BaseObj:
    def __init__(self) -> None:
        self._n = 42
    def value(self) -> int:
        return self._n
def maybe_value(x: BaseObj | None) -> int | None:
    # मामला: x None है => None लौटता है
    # मामला: x None नहीं है => 42 लौटता है
    # फिर भी: mypy आपत्ति करता है...
    #   Incompatible return value type (got BaseObj | int | None), expected "int | None"
    return x and x.value()
ऐसा क्यों होता है
x and x.value() जैसी अभिव्यक्ति रनटाइम truthiness पर निर्भर करती है। अगर x truthy है, तो Python x.value() का मूल्यांकन करके वही लौटाता है; अगर x falsy है, तो यह स्वयं x को लौटा देता है। चूँकि BaseObj __bool__ या __len__ परिभाषित नहीं करता, BaseObj का इंस्टेंस वास्तव में truthy होता है। इसलिए यह मान लेना आसान है कि यह अभिव्यक्ति केवल int या None ही देगी।
पेच subclassing में है। कोई subclass __bool__ को ओवरराइड करके इंस्टेंस को falsy बना सकता है। उस स्थिति में शॉर्ट‑सर्किट value() बुलाने के बजाय उसी इंस्टेंस को लौटा देगा, और रनटाइम पर अभिव्यक्ति का प्रकार सचमुच BaseObj | int | None बन सकता है।
class DerivedObj(BaseObj):
    def __bool__(self) -> bool:
        return False
reveal_type(maybe_value(DerivedObj()))  # "int | None" type checking के समय,
                                        # लेकिन रनटाइम पर DerivedObj.
mypy यहाँ सावधान रुख अपनाता है, और वजह वाजिब है। क्योंकि BaseObj को subclass किया जा सकता है और subclasses __bool__ बदल सकते हैं, mypy को उस शाखा का हिसाब रखना पड़ता है जहाँ बायाँ ऑपरेन्ड falsy है और जैसा है वैसा ही लौटा दिया जाता है।
समाधान
अगर आप चाहते हैं कि mypy यह माने कि इंस्टेंस तब तक falsy नहीं होंगे जब तक वे None न हों, तो ऐसी subclassing को रोकें जो truthiness बदल दे। क्लास को @final चिह्नित करें। final क्लास के साथ mypy सुरक्षित रूप से निष्कर्ष निकाल सकता है कि non-None इंस्टेंस के लिए bool(x) हमेशा True होगा और उसी अनुसार टाइप को संकीर्ण कर सकता है।
from typing import final
@final
class BaseObj:
    def __init__(self) -> None:
        self._n = 42
    def value(self) -> int:
        return self._n
def maybe_value(x: BaseObj | None) -> int | None:
    return x and x.value()
optional_obj and optional_obj.attribute को लिखने के किसी अलग तरीके के लिए, देखें: Pylance को None की संभावना नज़रअंदाज़ करने के लिए मैं क्या करूँ?.
यह क्यों महत्वपूर्ण है
स्टैटिक विश्लेषण को सभी वैध रनटाइम रास्तों को कवर करना होता है। subclassing की मौजूदगी का मतलब है कि truthiness के बारे में आपकी पक्की लगने वाली धारणा मूल क्लास बदले बिना भी टूट सकती है। इस किनारे के मामले को समझना चौंकाने वाली टाइप त्रुटियों से बचाता है और उससे भी बढ़कर, तब होने वाली सूक्ष्म रनटाइम बगों से बचाता है जब कोडबेस में falsy subclasses आ जाते हैं।
मुख्य बातें
अगर कोई शॉर्ट‑सर्किट अभिव्यक्ति किसी ऑब्जेक्ट और किसी वैल्यू को मिलाती है, तो जब वह ऑब्जेक्ट falsy हो सकता है, परिणाम स्वयं वही ऑब्जेक्ट भी हो सकता है। mypy इसे फ़्लैग करता है क्योंकि subclassing truthiness बदल सकती है। यदि क्लास का विस्तार करना उद्देश्य नहीं है या आप टाइप नॅरोइंग के लिए स्थिर truthiness की गारंटी चाहते हैं, तो उसे @final घोषित करें। और जब optionals पर एट्रिब्यूट एक्सेस करने की अलग शैली चाहिए, तो and-चेनिंग पर निर्भर रहने के बजाय संदर्भित सामग्री में दिए पैटर्न पर विचार करें।