2025, Nov 01 09:01
Protocol आधारित समाधान: raw कक्षाओं से processed टाइप
रनटाइम dictionary के बजाय Protocol और generics से raw कक्षाओं को processed कक्षाओं से जोड़ें। टाइप चेकर को सटीक रिटर्न टाइप मिले, overload के बिना, मजबूती से.
कई “raw” (कच्ची) कक्षाओं को उनकी प्रोसेस्ड समकक्ष कक्षाओं से जोड़ना रनटाइम पर तो आसान लगता है, लेकिन जैसे ही आप चाहते हैं कि टाइप चेकर इनपुट के आधार पर रिटर्न टाइप निकाल दे, बात बिगड़ जाती है। कक्षा‑से‑कक्षा मैपिंग का साधारण dictionary स्थिर विश्लेषण में कोई काम नहीं आता, और फ़ंक्शन को generic इनपुट के साथ अस्पष्ट रिटर्न टाइप देकर आप वहाँ Any पर पहुँच जाते हैं, जहाँ आपको सटीक प्रसार चाहिए था.
Problem setup
लक्ष्य यह है कि हम किसी raw टाइप का instance दें और टाइप चेकर लौटने वाले ठीक‑ठीक प्रोसेस्ड टाइप को समझ ले। एक सीधा‑सादा तरीका यह होगा कि मैपिंग को dictionary से बताएं और एक ही conversion फ़ंक्शन रखें:
from typing import Any
class X:
pass
class Y:
pass
class PreX:
pass
class PreY:
pass
mappings = {
PreX: X,
PreY: Y,
}
def materialize[TOrig: Any](item: TOrig) -> Any:
...
रनटाइम पर dictionary ठीक काम करती है, लेकिन टाइप चेकर उसे materialize के रिटर्न टाइप को संकरा करने के लिए नहीं पढ़ेगा। आप overload से सभी मामले गिना सकते हैं, पर सेट बढ़ने पर उसे हाथ से संभालना झंझट भरा और गलती‑संगत है.
Why the mapping dictionary doesn’t drive types
टाइप चेकर जनरिक्स निकालने के लिए रनटाइम dictionaries नहीं पढ़ते। कक्षा‑से‑कक्षा की तालिका केवल निष्पादन के समय मौजूद होती है, जबकि टाइपिंग को एक स्थिर अनुबंध चाहिए जो raw टाइप के instance को उसके संबंधित प्रोसेस्ड टाइप से जोड़ दे। इस अनुबंध के बिना चेकर materialize को Any या किसी व्यापक टाइप को लौटाते हुए देखता है, और आप सटीक टाइप‑संकीर्णन के फायदे खो देते हैं.
A protocol-based solution
विचार यह है कि मैपिंग को टाइप सतह में ही समा दिया जाए: हर raw कक्षा में एक साझा attribute हो जो उसकी प्रोसेस्ड कक्षा की ओर इशारा करे। फिर उस अपेक्षा को एक ऐसे Protocol से व्यक्त करें जो प्रोसेस्ड टाइप पर generic हो। फ़ंक्शन जैसे ही उस Protocol को स्वीकार करेगा, टाइप पैरामीटर सही ठोस टाइप में सुलझ जाएगा.
class X: ...
class Y: ...
class PreX:
bind = X # प्रोसेस्ड टाइप के लिए क्लास-स्तरीय कड़ी
class PreY:
bind = Y # प्रोसेस्ड टाइप के लिए क्लास-स्तरीय कड़ी
from typing import Protocol
class Carrier[T](Protocol):
@property
def bind(self) -> type[T]: ...
def materialize[T](item: Carrier[T]) -> T: ...
reveal_type(materialize(PreX())) # X
reveal_type(materialize(PreY())) # Y
इस तरह मैपिंग स्वयं टाइप्स में ही एन्कोड हो जाती है। हर raw कक्षा में एक class attribute होता है जो लक्षित ठोस टाइप बताता है; Protocol उस संबंध को परिभाषित करता है, और generic फ़ंक्शन ठीक वही लक्षित टाइप लौटाता है.
क्लास‑स्तरीय attributes और instance प्रोटोकॉल के बीच एक बारीकी है। प्रोसेस्ड टाइप से जोड़ने वाला attribute कक्षा पर परिभाषित है, पर उसे instances के जरिए एक्सेस किया जाता है। नतीजतन, इस पैटर्न की typing surface में उसे class variable के रूप में साफ‑साफ घोषित नहीं किया जा सकता। जैसा बताया गया, यहाँ “instance से class‑level प्रोटोकॉल” वाला संबंध व्यक्त करने का कोई तरीका नहीं है.
“instance से class‑level प्रोटोकॉल” संबंध (जैसे, Instance[Raw[T]]) व्यक्त करने का कोई तरीका नहीं है।
Why this approach matters
जब आपको टाइप चेकर से raw से processed टाइप्स की मैपिंग का अनुसरण कराना हो—वह भी बिना भारी‑भरकम overload सूचियों के—तो Protocol एक ऐसा संरचनात्मक अनुबंध देता है जो आसानी से स्केल करता है। मैपिंग टाइप्स के पास ही रहती है, अलग रनटाइम dictionary में नहीं, इसलिए आपके static analysis टूल इनपुट से आउटपुट तक T को प्रसारित कर पाते हैं। यह पैटर्न Mypy, Pyright और Pyrefly जैसे आधुनिक टाइप चेकरों के साथ सुथरे ढंग से काम करता है.
Takeaways
मैपिंग को रनटाइम‑केवल संरचना में नहीं, टाइप सिस्टम के भीतर रखें। हर raw कक्षा में एक साझा attribute दें जो उसकी प्रोसेस्ड कक्षा की ओर इंगित करे। उस अपेक्षा को एक generic Protocol से व्यक्त करें और अपने conversion फ़ंक्शन को उसी Protocol के संदर्भ में type करें। अगर आपने overload पर विचार किया है, तो यह विकल्प मामलों की नाज़ुक गिनती से बचाता है और फिर भी रिटर्न टाइप्स को सटीक रखता है। नतीजा: एक सरल, संभालने योग्य अनुबंध जो स्थिर विश्लेषण के अनुकूल रहता है.