Реестр Python удерживает объекты: когда подходит WeakValueDictionary
Компонент сервиса создаёт обработчики и регистрирует их по идентификатору. Работа завершилась, локальные ссылки исчезли, но память остаётся занятой: обычный словарь реестра продолжает владеть каждым обработчиком. При этом реестр нужен только для поиска тех объектов, которые уже используются другими частями приложения.
Сначала определить владельца
Обычный dict удерживает ключи и значения сильными ссылками. Пока словарь жив, его значения остаются достижимыми, даже если вся полезная работа с ними закончилась. Если реестр служит владельцем объектов, это правильное поведение и освобождение нужно выразить явным remove. Если он служит только дополнительным индексом, такая связь может оказаться лишней.
WeakValueDictionary хранит значения через слабые ссылки. Он позволяет найти объект, пока тот удерживается где-то ещё, но сам не продлевает его жизнь. Это подходит для наблюдательного реестра или повторного использования уже живущих объектов. Для кэша с обещанным сроком хранения такой подход меняет контракт: запись может исчезнуть сразу после ухода последнего сильного владельца.
Слабые ссылки поддерживаются не всеми типами. Пользовательские классы обычно подходят; для класса со __slots__ нужно предусмотреть __weakref__. Нельзя просто заменить любой словарь списков на WeakValueDictionary: обычный list не принимает слабую ссылку. В таких случаях сначала определите объект с понятным сроком жизни и ролью, а затем выбирайте способ индексирования.
Прочитать значение и удержать его
Пример регистрирует обработчик отчёта. После удаления исходной переменной полученная через get локальная ссылка ещё сохраняет объект. Только когда удалена и она, сборка мусора позволяет исчезнуть записи. Это важно для рабочего кода: извлеките значение один раз, проверьте на None и выполняйте операцию через ту же локальную переменную.
Два отдельных обращения вида «если ключ есть, взять по ключу» создают ненужный разрыв. Между ними другие владельцы могут освободить объект. В конкурентной программе дополнительно понадобится защита общей структуры и правил создания: WeakValueDictionary не заменяет протокол синхронизации. В данном примере один поток, поэтому проверяется именно владение, без примеси планировщика.
import gc
from weakref import WeakValueDictionary, ref
class Handler:
def __init__(self, name):
self.name = name
def render(self, values):
return f"{self.name}: {sum(values)}"
registry = WeakValueDictionary()
handler = Handler("report")
registry["daily"] = handler
observer = ref(handler)
current = registry.get("daily")
assert current is handler
del handler
gc.collect()
assert observer() is current
result = current.render([10, 20])
assert result == "report: 30"
del current
gc.collect()
assert observer() is None
assert registry.get("daily") is None
print(result, len(registry))Невидимые сильные ссылки
Если объект не исчезает, посмотрите на замыкания, списки задач и обработчики событий. Callback, который замкнул тот же объект, может удерживать его независимо от реестра. Добавление слабого словаря лишь уберёт одну из связей. Для диагностики полезно изолировать создание и завершение в маленьком сценарии, чтобы увидеть, какая переменная или коллекция остаётся владельцем.
Объекты с внешними ресурсами всё равно требуют явного завершения. Исчезновение слабой записи не является подходящим сигналом для своевременного закрытия соединения или подтверждения задания. Для таких действий используйте контекстный менеджер либо явный close в согласованном месте. Слабая ссылка управляет возможностью найти объект, а обязательства перед внешним сервисом задаются жизненным циклом операции.
В тестах не привязывайтесь к мгновенному освобождению на конкретной строке без учёта реализации Python. gc.collect здесь делает фазу проверки явной, а наблюдатель ref подтверждает, что исчез именно объект, а не только ключ в словаре. Проверяйте также случай с живым владельцем: реестр должен продолжать возвращать тот же экземпляр, пока приложение действительно использует его.
Реестр особенно полезен там, где главный владелец уже определён: например, активная операция держит обработчик до завершения, а диагностическая панель должна находить только действующие обработчики по имени. Если же панель сама становится единственным владельцем локальной переменной после get, объект проживёт до конца её операции — это нормальное следствие сильной ссылки. Не пытайтесь заставить слабый реестр удалять объект, пока с ним работают. Для метрик количества живых объектов избегайте долгого сохранения списка значений реестра: такой снимок сам продлевает им жизнь. Считайте нужный показатель быстро и освобождайте временные ссылки. Объяснённое владение позволяет отличить настоящую утечку от вполне ожидаемого удержания во время наблюдения за приложением.
Следующий шаг в практике
В открытом уроке о классах и экземплярах закрепите создание объектов с отдельным состоянием. Затем используйте программу продвинутого Python для работы с моделями и тестами. В «Python для профессионалов» изучают память, внутреннее устройство интерпретатора и профилирование — следующий шаг для поиска настоящих владельцев растущего набора объектов.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
