2025, Sep 24 01:31

XOR इनपुट के साथ lazy लोडिंग वाला Pydantic मॉडल कैसे बनाएं

सीखें कैसे Python Pydantic में XOR नियम लागू कर के या तो ID लें या पूरा ऑब्जेक्ट, और lazy लोडिंग व प्राइवेट cache से महंगा I/O टालें। टाइप-सेफ साफ डिज़ाइन टिप्स

ऐसा मॉडल बनाना, जो दो इनपुट में से ठीक एक स्वीकार करे और बाकी हिस्से को बाद में पूरा करे, सुनने में आसान लगता है — जब तक टाइप‑चेकर और वैलिडेशन नियम आड़े न आ जाएँ। लक्ष्य साफ है: या तो ID लें या पूरा ऑब्जेक्ट, एक समय में सिर्फ एक ही स्वीकार कराएँ, और महंगी फ़ेचिंग को lazy रखें ताकि I/O सिर्फ ज़रूरत पड़ने पर ही हो।

समस्या की रूपरेखा

शुरुआती तरीका private एट्रिब्यूट्स और computed प्रॉपर्टीज़ पर आधारित है। दिखने में यह कामचलाऊ लगता है, लेकिन static typing आपत्तियाँ उठाता है और दोनों इनपुट के बीच XOR नियम लागू कराना असहज हो जाता है।

from pydantic import BaseModel, model_validator
from typing import Optional
class Asset:
    ident: int
def load_asset_by_id(ident: int) -> Asset:
    ...
class AssetEnvelope(BaseModel):
    _asset_id: Optional[int] = None
    _asset: Optional[Asset] = None
    @model_validator(mode="before")
    @classmethod
    def ensure_reference(cls, payload):
        if payload.get("_asset_id") is None and payload.get("_asset") is None:
            raise ValueError("Define either _asset_id or _asset")
    @property
    def asset_id(self) -> int:
        if self._asset_id is None:
            self._asset_id = self.asset.ident
        return self._asset_id  # type checker: might still be None
    @asset_id.setter
    def asset_id(self, ident: int):
        self._asset_id = ident
    @property
    def asset(self) -> Asset:
        if self._asset is None:
            self._asset = load_asset_by_id(self.asset_id)
        return self._asset  # type checker: might still be None
    @asset.setter
    def asset(self, obj: Asset):
        self._asset = obj
AssetEnvelope(_asset_id=5)

कहां गड़बड़ होती है और क्यों

यहां दो आपस में गुंथी दिक्कतें हैं। पहली, दोनों इनपुट के बीच XOR बाधा पेचीदा हो जाती है जब मॉडल उन्हें private एट्रिब्यूट्स में रखता है। चाहत यह है कि एक लें और दूसरा नहीं — न दोनों, न दोनों में से कोई नहीं। दूसरी, lazy गेटर्स अवधारणा के लिहाज़ से सही हैं, मगर टाइप‑चेकर यह दिखाते हैं कि लौटाई गई वैल्यू अब भी None हो सकती है, क्योंकि Optional की स्थिति टाइप सिस्टम को स्पष्ट नहीं दिखती। नतीजा यह कि प्रॉपर्टीज़ रनटाइम पर तो काम करती हैं, लेकिन स्टैटिक विश्लेषण के समय संभावित None की चेतावनी देती रहती हैं। इसके अलावा, ID से ऑब्जेक्ट निकालने वाला महंगा I/O तब तक नहीं चलना चाहिए जब तक ऑब्जेक्ट सच में एक्सेस न किया जाए।

XOR लागू रखने और lazy बने रहने वाला समाधान

विचार सीधा है। पहचानकर्ता के लिए एक वास्तविक फ़ील्ड रखें और ऑब्जेक्ट के लिए aliased इनपुट लें। फ़ेच को lazy बनाए रखने के लिए रिज़ॉल्व ऑब्जेक्ट को रखने वाला private cache रखें। मॉडल वैलिडेटर में XOR संबंध लागू करें। अगर ऑब्जेक्ट दिया गया है, तो पहचानकर्ता उसी से सेट कर दें ताकि आगे का कोड पहचानकर्ता को मौजूद माने और Optional का शोर न रहे।

from __future__ import annotations
from pydantic import BaseModel, Field, PrivateAttr, model_validator
from typing import Optional
class Resource:
    key: int
def resolve_resource(key: int) -> Resource:
    ...
class ResourceCarrier(BaseModel):
    ref_id: Optional[int] = None
    _res_in: Optional[Resource] = Field(default=None, alias="resource")
    _res_cache: Optional[Resource] = PrivateAttr(None)
    @model_validator(mode="after")
    def _xor_and_populate(self):
        if (self.ref_id is None) == (self._res_in is None):
            raise ValueError("provide exactly one of ref_id or resource")
        if self._res_in is not None:
            self.ref_id = self._res_in.key
        return self
    @property
    def resource(self) -> Resource:
        if self._res_cache is None:
            if self._res_in is not None:
                self._res_cache = self._res_in
            else:
                assert self.ref_id is not None
                self._res_cache = resolve_resource(self.ref_id)  # lazy I/O
        return self._res_cache

इस संरचना में मॉडल दो इनपुट में से ठीक एक ही स्वीकार करता है और आंतरिक अवस्था को सुसंगत रखता है। ResourceCarrier(ref_id=5) जैसा पहचानकर्ता देने पर महंगा कॉल तब तक टल जाता है जब तक resource पहली बार एक्सेस न हो। वहीं ResourceCarrier(resource=some_res) जैसा पूरा ऑब्जेक्ट देने पर ref_id को some_res.key से सेट कर दिया जाता है और कोई फ़ेच बिल्कुल नहीं होती।

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

यह पैटर्न एक साथ तीन व्यावहारिक चिंताएँ सुलझाता है। यह “सिर्फ एक इनपुट” का नियम एक जगह लागू करता है, जिससे अस्पष्ट अवस्था नहीं बनती। यह नेटवर्क या डेटाबेस कॉल को lazy रखता है, यानी I/O की कीमत तभी लगती है जब ऑब्जेक्ट वाकई चाहिए। और जब ऑब्जेक्ट दिया गया हो, तो वैलिडेशन के बाद पहचानकर्ता non‑optional हो जाता है, जिससे आगे के कोड को Optional की उठा‑पटक से छुटकारा मिलता है।

मुख्य बातें

जब किसी मॉडल की पहचान दो अदल‑बदल तरीकों से हो सके, तो कुंजी डेटा के लिए वास्तविक फ़ील्ड अपनाएँ, वैकल्पिक रूप को alias के जरिए लें, और XOR जाँच को वैलिडेटर में केंद्रीकृत करें। रिज़ॉल्व ऑब्जेक्ट के लिए आंतरिक cache रखें और ऐसी प्रॉपर्टी दें जो lazy लोडिंग करे। इससे वैलिडेशन स्पष्ट रहता है, महंगा काम टलता है, और बाकी कोड एक स्थिर, अनुमानित आकार पर भरोसा कर सकता है।

यह लेख StackOverflow के प्रश्न (द्वारा: Engensmax) और Dmitry543 के उत्तर पर आधारित है।