2025, Sep 27 03:17
Async __del__ в Python: почему не работает и очистка через __aexit__
Почему async __del__ в Python не выполняется и как правильно делать асинхронную очистку: используйте async with и __aexit__ вместо финализатора — надёжно.
Async __del__ в Python: что на самом деле происходит и как корректно выполнять очистку
Вы можете объявлять async def для многих dunder‑методов, но это не означает, что Python будет автоматически ожидать их завершения. Частый вопрос — сработает ли асинхронный __del__ во время сборки мусора. Короткий ответ: нет. Корутин, создаваемый async __del__, никогда не ожидается, поэтому код очистки внутри него просто не выполняется.
Минимальный пример, который выглядит правильно, но не запускается
Ниже упрощённый класс с асинхронным __del__:
class ResourceSlot:
    async def __del__(self):
        print("Async __del__ called")
obj = ResourceSlot()
del obj
Синтаксис корректный, но тело __del__ не выполнится. В момент финализации объекта этот корутин никто не ожидает, поэтому сообщение так и не будет напечатано. В лучшем случае при завершении программы вы увидите предупреждение о том, что корутина ни разу не ожидалась.
Почему всё устроено именно так
В Python существует чёткий контракт для dunder‑методов: интерпретатор вызывает их в определённых ситуациях и использует их результаты строго заданным образом. Отдельно async def определяет корутин‑функцию. Вызов такой функции немедленно возвращает объект корутины; её тело выполняется только тогда, когда корутину ожидают с помощью await.
Эти правила хорошо сочетаются в некоторых местах. Например, можно определить асинхронный __getitem__. При обращении по индексу вы получите корутину и сможете явно подождать её:
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__, попросту некому ожидать, и код внутри не выполняется.
Как правильно выполнять асинхронную очистку
Для асинхронной финализации используйте протокол асинхронного контекстного менеджера и размещайте очистку в __aexit__. Затем используйте объект внутри блока async with. Такой подход делает ожидание явным и гарантирует, что асинхронная очистка сработает вовремя.
Справка по протоколу: протокол асинхронного контекстного менеджера в документации Python: 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, поэтому ваша логика очистки выполняется надёжно. Такой подход предпочтительнее __del__ даже для чисто синхронных сценариев, потому что срок жизни ресурса становится явным и детерминированным.
Почему это важно
Опора на async __del__ создаёт иллюзию очистки без каких‑либо гарантий. Корутину никто не будет ожидать, значит, финализация не выполнится, и максимум вы увидите предупреждение о корутине, которую так и не ожидали, при выходе из программы. Перенос очистки в __aexit__ исключает тихие сбои и делает работу с ресурсами простой и проверяемой.
Итоги
Если вам нужна асинхронная очистка, не помещайте её в __del__. Используйте асинхронный контекстный менеджер и переносите финализацию в __aexit__, а жизненный цикл запускайте внутри async with. Если в других местах требуются «ленивые» асинхронные операции, паттерны вроде async __getitem__ работают потому, что вызывающий может явно их ожидать. Держите await там, где он действительно может быть выполнен, — и очистка сработает тогда, когда должна.
Статья основана на вопросе на StackOverflow от Cherry Chen и ответе от jsbueno.