Разберите последствия: что происходит с объектом, если его __del__ сохраняет сам объект во внешней переменной?
Объект воскресает: сохранение self во внешней переменной снова делает его достижимым, поэтому он не уничтожается в этот момент. Метод __del__ для одного объекта вызывается не более одного раза, поэтому после последующего удаления внешней ссылки финализатор обычно повторно не запускается.
Это делает __del__ опасным механизмом управления ресурсами: время его вызова недетерминировано, а объект может остаться жить дольше ожидаемого.
__del__ появился как способ выполнить пользовательскую логику перед уничтожением объекта, когда языку не хватает автоматического освобождения внешних ресурсов. Такой механизм особенно связан с реализациями, где объект может быть уничтожен сразу после исчезновения последней сильной ссылки.
Однако сборка мусора и время финализации зависят от реализации Python. Поэтому __del__ не является аналогом детерминированного блока очистки вроде контекстного менеджера.
Финализатор может получить частично разрушенный объект, особенно во время завершения интерпретатора. Если он сохранит self в глобальной переменной, кэше или другом долгоживущем объекте, экземпляр снова станет достижимым и его память не будет освобождена.
Это может привести к утечке памяти, удержанию больших графов объектов и сохранению уже закрытого или некорректно подготовленного ресурса. Дополнительный риск возникает, если код рассчитывает, что повторное исчезновение последней ссылки снова вызовет __del__.
При исчезновении последней сильной ссылки Python подготавливает объект к финализации и вызывает его __del__, если такой метод определён. Если во время этого вызова выполнить сохранение self во внешнем хранилище, объект получает новую сильную ссылку и снова становится достижимым.
В CPython при таком простом сценарии del obj обычно приводит к немедленному выводу finalized, после чего проверка печатает True. Последующее удаление resurrected делает объект недостижимым, но __del__ повторно вызываться не должен.
Точный момент первого вызова нельзя переносить между всеми реализациями Python: немедленная финализация является особенностью подсчёта ссылок CPython, а не универсальным контрактом языка. Исключения из __del__ не следует использовать для управления ошибками: они выводятся как предупреждения, а исключение не передаётся вызывающему коду.
Для освобождения файлов, соединений и блокировок предпочтителен контекстный менеджер с with: он задаёт явную границу очистки. Для фоновой финализации объектов иногда подходит weakref.finalize, но и она не должна быть единственной гарантией закрытия критически важного ресурса.
В кэше находился объект-обёртка над нативным ресурсом. Разработчик добавил __del__, который закрывал ресурс, а при диагностике сохранял self в глобальный список. После удаления объектов из основного кэша память не освобождалась, потому что диагностический список удерживал воскресшие экземпляры.
Рассматривались три варианта. Оставить очистку только в __del__ было просто, но не гарантировало момент освобождения и допускало воскресение. Использовать weakref.finalize уменьшало зависимость от метода объекта, но всё равно не заменяло явное закрытие. Контекстный менеджер требовал изменить API, зато делал жизненный цикл ресурса очевидным и детерминированным.
Выбрали контекстный менеджер для основного пути использования и дополнительный механизм финализации только как защитный барьер. В результате ресурс закрывался в конце блока, а диагностика больше не удерживала сами объекты.
Вызывается ли __del__ повторно после воскресения объекта?
Нет, полагаться на повторный вызов нельзя: для одного объекта финализатор вызывается не более одного раза. Поэтому воскресший объект может продолжить существование без возможности автоматически повторить прежнюю очистку.
Гарантирует ли __del__ освобождение ресурса при циклических ссылках?
Нет. Современный Python умеет собирать многие циклы с финализаторами, но момент вызова остаётся недетерминированным, а воскресение может сделать объект снова достижимым. Поэтому наличие __del__ не даёт гарантии своевременного закрытия файла, сокета или блокировки.
Почему __del__ особенно ненадёжен при завершении интерпретатора?
Во время завершения глобальные модули и их атрибуты могут уже быть удалены или заменены на None. Финализатор может выполняться в порядке, который не соответствует порядку создания объектов, поэтому ссылки на внешние глобальные зависимости могут быть недоступны. Критическую очистку следует выполнять явно до завершения интерпретатора, а __del__ рассматривать только как необязательный резервный механизм.