2025, Sep 27 03:31

Python में async __del__ असल में क्या होता है और सही cleanup कैसे करें

जानिए क्यों Python में async __del__ काम नहीं करता और उसका coroutine await नहीं होता. भरोसेमंद async cleanup के लिए async context manager व __aexit__ अपनाएँ.

Python में Async __del__: असल में क्या होता है और सही तरीके से क्लीनअप कैसे करें

कई dunder methods के लिए आप async def लिख सकते हैं, लेकिन इसका मतलब यह नहीं कि Python उन्हें अपने‑آپ await भी कर देगा। अक्सर पूछा जाता है कि क्या garbage collection के दौरान async __del__ चलता है। छोटा जवाब: नहीं। async __del__ से जो coroutine ऑब्जेक्ट बनता है, उसे कभी await ही नहीं किया जाता, इसलिए उसके अंदर का cleanup कोड चलता ही नहीं।

ऐसा छोटा उदाहरण जो ठीक लगता है, पर चलता नहीं

यहाँ asynchronous __del__ वाली एक संक्षिप्त class है:

class ResourceSlot:
    async def __del__(self):
        print("Async __del__ called")
obj = ResourceSlot()
del obj

यह सिंटैक्स मान्य है, मगर __del__ का शरीर नहीं चलेगा। ऑब्जेक्ट के अंतिमकरण के समय उस coroutine को कोई await नहीं करता, इसलिए संदेश कभी छपता नहीं। प्रोग्राम बंद होते वक्त अधिकतम आपको इतना दिखेगा कि कोई चेतावनी आई है: एक coroutine को कभी await नहीं किया गया।

ऐसा व्यवहार क्यों होता है

dunder methods के लिए Python का अनुबंध साफ है: इंटरप्रेटर उन्हें तय परिस्थितियों में कॉल करता है और उनके रिटर्न मान का उपयोग खास तरीकों से करता है। दूसरी ओर, async def एक coroutine function परिभाषित करता है। ऐसे फ़ंक्शन को कॉल करते ही एक coroutine ऑब्जेक्ट लौटता है; उसका शरीर तभी चलता है जब उस coroutine को await किया जाए।

कुछ जगह ये नियम साथ‑साथ काम करते हैं। उदाहरण के लिए, आप async __getitem__ बना सकते हैं। जब आप ऑब्जेक्ट को index करते हैं, तो एक coroutine लौटता है और आप उसे स्पष्ट रूप से await कर सकते हैं:

class LazyStore:
    async def __getitem__(self, key):
        print("getting:", key)
        return f"value:{key}"
async def run_lookup():
    store = LazyStore()
    result = await store["alpha"]
    print(result)

यह पैटर्न इसलिए काम करता है क्योंकि await का नियंत्रण कॉलर के पास होता है। लेकिन __del__ के साथ इंटरप्रेटर रिटर्न मान को नजरअंदाज करता है और कोई await स्थान होता ही नहीं। async __del__ से बना coroutine कहीं भी await नहीं किया जाता, इसलिए उसका भीतर का कोड नहीं चलता।

Async cleanup करने का सही तरीका

असिंक्रोनस फ़ाइनलाइज़ेशन के लिए asynchronous context manager प्रोटोकॉल अपनाएँ और cleanup को __aexit__ में रखें। फिर उस ऑब्जेक्ट का उपयोग async with ब्लॉक के अंदर करें। यह डिज़ाइन await को स्पष्ट बनाता है और सुनिश्चित करता है कि आपका async cleanup सही समय पर चले।

प्रोटोकॉल संदर्भ: Python दस्तावेज़ में asynchronous context manager प्रोटोकॉल: asynchronous context manager.

class AsyncGuard:
    def __init__(self, name):
        self.name = name
    async def __aenter__(self):
        print(f"enter: {self.name}")
        return self
    async def __aexit__(self, exc_type, exc, tb):
        print(f"cleanup: {self.name}")
async def main_task():
    async with AsyncGuard("session-1") as g:
        print("work happens here")

इस पैटर्न में __aexit__ को async with की मशीनरी await करती है, इसलिए आपका cleanup लॉजिक भरोसेमंद तरीके से चलता है। यह तरीका शुद्ध synchronous परिदृश्यों में भी __del__ से बेहतर है, क्योंकि यह संसाधनों की उम्र को स्पष्ट और निर्धार्य बनाता है।

यह आपके लिए क्यों मायने रखता है

async __del__ पर निर्भर रहना cleanup का भ्रम तो देता है, पर कोई गारंटी नहीं। coroutine await नहीं होगा, यानी आपका finalization कोड चलेगा ही नहीं, और प्रोग्राम बंद होते समय संभव है कि बस एक चेतावनी दिखे कि coroutine को await नहीं किया गया। cleanup को __aexit__ में ले जाने से ऐसे मौन असफलताएँ नहीं होतीं और संसाधन प्रबंधन सरल व परीक्षणयोग्य बनता है।

निष्कर्ष

अगर आपको async cleanup चाहिए, तो उसे __del__ में न रखें। asynchronous context manager का उपयोग करें और finalization लॉजिक को __aexit__ में रखें, तथा lifetime को async with के भीतर चलाएँ। जहाँ आलसी async ऑपरेशनों की ज़रूरत हो, वहाँ async __getitem__ जैसे पैटर्न काम करते हैं क्योंकि कॉलर उन्हें स्पष्ट रूप से await कर सकता है। await वहीं रखें जहाँ वे वाकई हो सकें, और आपका cleanup समय पर चलेगा।

यह लेख StackOverflow के एक प्रश्न (लेखक: Cherry Chen) और jsbueno के उत्तर पर आधारित है।