2025, Oct 20 11:31

Python में इंस्टेंस एनोटेशन से टाइप एलियस: Annotated और TypeAliasType

Python में इंस्टेंस एनोटेशन को क्लास एट्रिब्यूट की तरह एक्सेस करने से होने वाली AttributeError का हल सीखें: __annotations__, Annotated, TypeAliasType व NewType से मजबूत टाइप एलियस और ID रेफरेंसिंग बनाएं.

जब आप ऑब्जेक्ट रेफरेंसेज़ को टाइप किए हुए ID—RDF जैसी URI रेफरेंसिंग—की तरह मॉडल करते हैं, तो सीधे किसी क्लास से फ़ील्ड का टाइप उठा लेने का मन होता है। लेकिन इंस्टेंस एनोटेशन क्लास एट्रिब्यूट नहीं होते, और उन्हें सीधे पकड़ने की कोशिश अंततः विफल होती है। यहां एक व्यावहारिक पैटर्न है, जिससे आप किसी क्लास के एनोटेटेड फ़ील्ड से AttributeError से बचे रहते हुए टाइप एलियस निकाल सकते हैं।

समस्या का सेटअप

उद्देश्य है: एक क्लास और उसके गैर-लिटरल एट्रिब्यूट को टाइप्ड आइडेंटिफ़ायर की तरह मॉडल करना, और फिर ऐसा टाइप एलियस बनाना जो उसी फ़ील्ड की एनोटेशन को प्रतिबिंबित करे। नीचे के उदाहरण में ID के लिए टाइप किया हुआ integer इस्तेमाल हुआ है और Annotated के साथ मेटाडेटा जोड़ा गया है, ताकि सीरियलाइज़ेशन/डी-सीरियलाइज़ेशन के समय जाँच करना आसान रहे।

import inspect
import typing
from typing import Annotated, Any, NewType, TypeAliasType, get_args, get_origin
UidType = NewType("UidType", int)
class Human:
    key: UidType
    ancestors: list["HumanId"]
def slot_info(model_cls: type, field_name: str) -> tuple[type, str, Any]:
    hints = inspect.get_annotations(model_cls)
    assert field_name in hints
    return (model_cls, field_name, hints[field_name])
HumanId = TypeAliasType("HumanId", Annotated[UidType, slot_info(Human, "key")])
assert Human.__name__ == "Human"
assert isinstance(HumanId, typing.TypeAliasType)
orig = get_origin(HumanId.__value__)
params = get_args(HumanId.__value__)
assert orig is Annotated
assert params[0] is UidType
assert params[1] == (Human, "key", UidType)
assert params[0] == params[1][2]

नीचे जैसी सीधी कोशिश काम नहीं करेगी, क्योंकि वह फ़ील्ड क्लास एट्रिब्यूट नहीं है:

HumanId = TypeAliasType("HumanId", Human.key)  # AttributeError: टाइप ऑब्जेक्ट 'Human' में 'key' नाम का एट्रिब्यूट नहीं है

यह क्यों विफल होता है

अगर कोई मेंबर सिर्फ़ इंस्टेंस एट्रिब्यूट के रूप में annotate किया गया है, तो वह क्लास पर मौजूद नहीं होता। उसका टाइप क्लास की annotations mapping में रहता है, या इंस्टेंस पर असाइन होने के बाद, लेकिन क्लास एट्रिब्यूट के रूप में नहीं।

दूसरे शब्दों में, key इंस्टेंस-लेवल एनोटेशन है। Python इस जानकारी को क्लास के __annotations__ में दर्ज करता है, लेकिन key नाम का कोई क्लास एट्रिब्यूट नहीं बनाता। इसलिए Human.key एक्सेस करने पर AttributeError उठती है।

कारगर समाधान

एलियस को क्लास की annotations डिक्शनरी से हासिल करें। इससे आप ठीक वही टाइप संदर्भित करते हैं, जो एनोटेशन में इस्तेमाल हुआ था।

from typing import NewType
UidType = NewType("UidType", int)
class Human:
    key: UidType
    ancestors: list["HumanId"]
HumanId = Human.__annotations__["key"]

Python 3.12+ में, यही तरीका TypeAliasType के साथ भी सहजता से काम करता है:

from typing import NewType, TypeAliasType
UidType = NewType("UidType", int)
class Human:
    key: UidType
    ancestors: list["HumanId"]
HumanId = TypeAliasType("HumanId", Human.__annotations__["key"])

यदि आपको मेटाडेटा जोड़ना हो, तो बेस टाइप को Annotated से रैप करें। मेटाडेटा में आप वह सब रख सकते हैं, जिसकी मदद से यह सत्यापित हो कि संदर्भित फ़ील्ड के साथ एनोटेशन मेल खाता है।

from typing import Annotated, NewType, TypeAliasType
UidType = NewType("UidType", int)
class Human:
    key: UidType
    ancestors: list["HumanId"]
def slot_meta(model_cls: type, field_name: str):
    hints = model_cls.__annotations__
    assert field_name in hints
    return (model_cls, field_name, hints[field_name])
HumanId = TypeAliasType("HumanId", Annotated[UidType, slot_meta(Human, "key")])

यह क्यों महत्वपूर्ण है

डेटाबेस सीरियलाइज़ेशन/डी-सीरियलाइज़ेशन के आसपास टाइप वैलिडेशन करते समय यह भरोसा चाहिए कि आपका टाइप एलियस वास्तव में उसी फ़ील्ड की एनोटेशन को दर्शाता है। __annotations__ से टाइप निकालना यही संरेखण देता है और ऐसे नाज़ुक एट्रिब्यूट-लुकअप से बचाता है जो इंस्टेंस एनोटेशन के लिए विफल होंगे। TypeAliasType और Annotated के साथ, आप मूल टाइप के साथ-साथ आवश्यक मेटाडेटा भी रख सकते हैं, जिसे आपकी जाँच के लिए आसानी से इं्ट्रोस्पेक्ट किया जा सके।

मुख्य बातें

इंस्टेंस एनोटेशन को क्लास एट्रिब्यूट की तरह एक्सेस न करें। इसके बजाय, उन्हें क्लास की __annotations__ मैपिंग से पढ़ें और, Python 3.12+ पर, यदि स्पष्ट एलियस चाहिए तो TypeAliasType से रैप करें। जब मेटाडेटा मायने रखता हो, तो Annotated का उपयोग करें ताकि अतिरिक्त संदर्भ टाइप के साथ जुड़ा रहे। पृष्ठभूमि और औपचारिक विवरण के लिए PEP 526 पर variable annotations, PEP 613 पर explicit type aliases, और Python दस्तावेज़ देखें: Class __annotations__, typing.TypeAliasType, और typing.Annotated।

यह लेख StackOverflow के प्रश्न पर आधारित है, जिसे ticapix ने पूछा था, और Sithila Sihan Somaratne के उत्तर पर।