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.