2025, Oct 22 11:47
CPython में __new__ मंकी-पैच के बाद रिवर्ट क्यों फेल होता है और समाधान
Python में __new__ मंकी-पैच के बाद CPython में object.__new__ पर रीसेट क्यों TypeError देता है, कारण क्या है और क्लास को फिर से बनाकर सुरक्षित रिवर्ट कैसे करें.
Python में ऑब्जेक्ट निर्माण को मंकी-पैच करना तब तक सरल दिखता है, जब तक आप __new__ को नहीं छूते। दिखने में छोटा बदलाव—किसी क्लास के allocator को बदलना और फिर उसे वापस करना—CPython में TypeError दे सकता है, जबकि साफ शुरुआती स्थिति से वही काम ठीक चलता है। यह गाइड बताती है कि असल व्यवहार क्या है, व्यवहार में ऐसा क्यों होता है, और जब __new__ पैच हो चुका हो तो मूल सेमान्टिक्स को सुरक्षित तरीके से कैसे वापस लाया जाए।
समस्या कैसे दोहराएँ
मान लें कि आपके पास एक थर्ड-पार्टी क्लास है जिसे आप सीधे बदल नहीं सकते। आप ऑब्जेक्ट निर्माण को इंटरसेप्ट करने के लिए उसका __new__ पैच करते हैं और बाद में इसे डिफॉल्ट इम्प्लीमेंटेशन पर रीसेट करना चाहते हैं। पहला कदम उम्मीद के मुताबिक काम करता है।
class Alpha:
    def __init__(self, val):
        self.val = val
def hook_new(typ, *args, **kwargs):
    print("hook_new called")
    return object.__new__(typ)
Alpha.__new__ = hook_new
Alpha(10)
# hook_new बुलाया गया
# <__main__.Alpha ऑब्जेक्ट 0x... पर>
अब आप सीधे object.__new__ असाइन करके मूल स्थिति बहाल करने की कोशिश करते हैं। कॉल TypeError के साथ असफल हो जाती है।
Alpha.__new__ = object.__new__
Alpha(10)
# TypeError: object.__new__() को ठीक एक आर्ग्युमेंट चाहिए (जिस टाइप को इंस्टैंशिएट करना है)
हैरानी की बात यह है कि अगर शुरुआत से ही __new__ को object.__new__ पर सेट किया जाए, बिना पहले मंकी-पैच किए, तो सब ठीक चलता है।
class Beta:
    def __init__(self, val):
        self.val = val
Beta.__new__ = object.__new__
Beta(10)
# ठीक चलता है
असल में क्या हो रहा है
यह CPython-विशिष्ट विचित्रता है। किसी क्लास के __new__ को मंकी-पैच करने के बाद CPython आंतरिक तौर पर उस टाइप के allocation पाथ को “touched” के रूप में मार्क कर देता है। उसके बाद, भले ही आप बाद में object.__new__ ही वापस असाइन कर दें, कॉल पाथ अब कभी न बदले गए, स्वच्छ क्लास जैसा व्यवहार नहीं करता। फर्क आर्ग्युमेंट हैंडलिंग में दिखता है: जब __new__ कभी ओवरराइड नहीं हुआ हो, तो CPython object.__new__ को विशेष तरीके से ट्रीट करता है; लेकिन एक बार स्लॉट “used” के रूप में मार्क हो जाए, तो CPython अतिरिक्त आर्ग्युमेंट्स को पहले जैसी अनदेखी नहीं करता।
स्वाभाविक रूप से आपको लग सकता है कि मंकी-पैच किया गया एट्रिब्यूट डिलीट करने से व्यवहार वापस आ जाएगा, क्योंकि एट्रिब्यूट लुकअप object.__new__ पर फॉल बैक करना चाहिए। व्यवहार में, CPython में वह आंतरिक “touched” स्थिति बनी रहने के कारण एट्रिब्यूट हटाने पर भी मूल सेमान्टिक्स नहीं लौटते।
स्पष्ट रिवर्ट की कोशिश (और फिर भी क्यों विफल होती है)
इरादा है ओवरराइड हटाकर डिफॉल्ट allocator पर वापस जाना। CPython में ऐसा करने पर भी प्रारंभिक कॉल सेमान्टिक्स वापस नहीं आते।
class Alpha:
    def __init__(self, val):
        self.val = val
def hook_new(typ, *args, **kwargs):
    print("hook_new called")
    return object.__new__(typ)
Alpha.__new__ = hook_new
Alpha(10)
# वापस करने की कोशिश
del Alpha.__new__
Alpha(10)
# टाइप 'touched' होने के बाद भी CPython में TypeError उठती है
यह नतीजा CPython की आंतरिक कार्यप्रणाली और __new__ तक पहुँच के लिए किए गए अनुकूलनों से जुड़ा है—यह भाषा स्पेसिफिकेशन द्वारा सुनिश्चित गुण नहीं, बल्कि इम्प्लीमेंटेशन का साइड-इफेक्ट है।
व्यावहारिक वर्कअराउंड जो भरोसेमंद तरीके से व्यवहार बहाल करता है
पैच से पहले वाली सेमान्टिक्स पर लौटने के लिए क्लास ऑब्जेक्ट को फिर से बनाएँ। यह आप type को मूल क्लास का नाम, बेस क्लासेज़ और उसके namespace की एक शैलो कॉपी देकर कर सकते हैं। इससे एक नया टाइप बनता है, जिसका __new__ स्लॉट अब तक “touched” नहीं हुआ होता।
class RootBase:
    pass
class Alpha(RootBase):
    def __init__(self, val):
        self.val = val
def hook_new(typ, *args):
    print("hook_new called", *args)
    return object.__new__(typ)
# पैच
Alpha.__new__ = hook_new
Alpha(23)
# एट्रिब्यूट हटाएँ; CPython में यह पर्याप्त नहीं है
del Alpha.__new__
# यहाँ Alpha(23) CPython में अब भी फेल होगा
# इसके हिस्सों से क्लास फिर से बनाएँ
Alpha = type(Alpha.__name__, Alpha.__bases__, dict(Alpha.__dict__))
# अब फिर से मूल सेमान्टिक्स के साथ काम करता है
Alpha(23)
इम्प्लीमेंटेशन पर नोट्स
यह असर CPython की इम्प्लीमेंटेशन-लेवल डिटेल्स से जुड़ा है और बग रिपोर्ट का उम्मीदवार भी माना जा सकता है। एक और प्रसिद्ध Python इम्प्लीमेंटेशन, PyPy, यह व्यवहार नहीं दिखाता। PyPy में __new__ को मंकी-पैच करने के बाद केवल एट्रिब्यूट डिलीट कर देने से सामान्य कंस्ट्रक्शन वापस आ जाता है।
class Gamma:
    def __init__(self, val):
        self.val = val
def hook_new(typ, *args):
    print("hook_new called", *args)
    return object.__new__(typ)
Gamma.__new__ = hook_new
Gamma(23)  # संदेश प्रिंट होता है
del Gamma.__new__
Gamma(23)  # PyPy पर फिर से काम करता है
प्रोडक्शन कोड के लिए यह क्यों मायने रखता है
यदि आप रनटाइम पर थर्ड-पार्टी क्लासेज़ को इंस्ट्रूमेंट या हॉट-फिक्स कर रहे हैं, तो कंस्ट्रक्टर पाथ सबसे नाज़ुक जगह है। CPython का __new__ के साथ आंतरिक व्यवहार मतलब यह है कि दिखने में रिवर्सिबल पैच साधारण री-असाइनमेंट या डिलीशन से सच में उलटा नहीं किया जा सकता, और आपका कोड एक इंटरप्रेटर पर चलेगा, दूसरे पर नहीं—या उल्टा। इस किनारे के केस की जानकारी होने से ऑब्जेक्ट निर्माण के दौरान उलझाने वाली TypeError क्रैश से बचाव होता है, खासकर लंबे समय तक चलने वाली प्रक्रियाओं में जो डायनैमिक रूप से क्लासेज़ बदलती हैं।
व्यावहारिक दिशा-निर्देश और निष्कर्ष
यदि आपको __new__ के जरिए ऑब्जेक्ट निर्माण इंटरसेप्ट करना ही पड़े, तो CPython में उस क्लास को पूरे प्रोसेस की अवधि तक “tainted” मानें। यह न मानें कि __new__ को वापस object.__new__ पर सेट करना या एट्रिब्यूट हटाना मूल व्यवहार को पूरी तरह लौटा देगा। जब आपको साफ शुरुआत चाहिए, तो क्लास ऑब्जेक्ट को उसके मौजूदा नाम, बेस क्लासेज़ और उसके dict की शैलो कॉपी के साथ type से फिर से बनाएँ। यदि आपका कोड कई Python इम्प्लीमेंटेशन पर चलता है, तो अपने CI मैट्रिक्स में इस व्यवहार को वेरिफाई करें, क्योंकि PyPy में यह समस्या नहीं दिखती और जहाँ CPython फेल होता है वहाँ वह पास हो सकता है।
संक्षेप में, __new__ का मंकी-पैचिंग शक्तिशाली है, लेकिन इंटरप्रेटर-विशिष्ट सेमान्टिक्स के साथ आती है। अगर रिवर्सिबिलिटी चाहिए, तो एट्रिब्यूट डिलीट या री-असाइन करने की बजाय क्लास को फिर से बनाने पर भरोसा करें।