В реестре обработчиков сохраняют метод экземпляра, после чего экземпляр не освобождается: какой механизм удерживает его в памяти?
Сохранённый связанный метод удерживает ссылку на свой экземпляр через атрибут __self__. Поэтому экземпляр не может быть освобождён, пока реестр хранит этот метод или другой объект, ссылающийся на него.
Методы экземпляров в Python проектировались как вызываемые объекты, автоматически связывающие функцию класса с конкретным экземпляром. Такой механизм позволяет передавать obj.method как callback без явной передачи obj при каждом вызове.
Удобство этой модели имеет важное следствие для управления памятью: полученный связанный метод — не просто ссылка на функцию, а объект, содержащий ссылку на экземпляр и функцию класса.
Проблема возникает, когда обработчики сохраняются надолго: в диспетчере событий, планировщике задач, пуле callback-функций или глобальном реестре. Даже если все явные ссылки на экземпляр удалены, реестр продолжает владеть связанным методом.
В результате экземпляры, содержащие большие буферы, соединения или другие ресурсы, остаются живыми дольше ожидаемого. Это может выглядеть как утечка памяти, хотя сборщик мусора работает корректно: объект всё ещё достижим по цепочке ссылок.
При обращении к obj.method дескриптор метода формирует связанный метод. У него есть ссылка на функцию класса и ссылка на объект obj, который будет передан функции как self.
После удаления переменной worker экземпляр остаётся достижимым через callbacks[0].__self__. Очистка реестра удаляет последнюю такую ссылку, после чего объект может быть освобождён; для обычного экземпляра без циклических ссылок это обычно происходит сразу благодаря подсчёту ссылок в CPython.
Практическое решение зависит от требуемой семантики владения:
weakref.WeakMethod для слабого хранения связанного метода;Слабые ссылки подходят не всегда. Если объект должен гарантированно жить, пока зарегистрирован обработчик, сильная ссылка является правильной и полезной. Кроме того, слабая ссылка требует обрабатывать ситуацию, когда экземпляр уже уничтожен.
Важно отличать удержание от циклической ссылки. Если реестр живёт дольше экземпляра и содержит его связанный метод, это обычная достижимая ссылка, поэтому запуск сборщика циклов сам по себе проблему не решит.
В сервере объекты подписчиков регистрировали свои методы в глобальном диспетчере событий. После завершения запросов подписчики удалялись из основного списка, но диспетчер продолжал хранить callbacks, из-за чего каждый подписчик вместе с кэшем данных оставался в памяти.
Рассматривались три варианта. Периодически запускать сборщик циклов было бесполезно: объекты не были недостижимыми. Полностью очищать глобальный список было опасно, поскольку это удаляло активные подписки. Явная отписка давала предсказуемое владение, но требовала гарантированного вызова в каждом пути завершения.
Выбрали явную отписку для обязательных подписок и WeakMethod для необязательных наблюдателей. Это сохранило активные подписки до их штатного удаления и позволило автоматически исчезать наблюдателям, чей владелец больше нигде не использовался.
Нет. Удаление переменной убирает только одну ссылку. Если связанный метод сохранён в списке, словаре, очереди задач или замыкании, он по-прежнему содержит ссылку на экземпляр через __self__.
gc.collect() в такой ситуации?Нет, если экземпляр остаётся достижимым через реестр обработчиков. Сборщик циклов удаляет недостижимые циклические структуры, но не должен уничтожать объект, на который всё ещё ссылается живой реестр. Нужно найти и удалить удерживающую ссылку.
weakref.ref отличается от weakref.WeakMethod для этой задачи?weakref.ref обычно применяют к экземпляру, но вызов метода через такую ссылку требует отдельно получить объект и обратиться к его методу. weakref.WeakMethod специально представляет слабую ссылку на связанный метод: при вызове она возвращает временный вызываемый метод, если экземпляр ещё жив, либо None. Это удобнее для реестров callbacks, но код должен корректно обрабатывать исчезновение владельца.