Команда Python for Devs подготовила перевод статьи о слабых ссылках в Python и управлении памятью. В материале разбирается, как работает подсчёт ссылок, почему циклические зависимости приводят к утечкам памяти и в каких случаях weak references становятся незаменимым инструментом.
Работая с Python (как и со многими другими языками), вы обычно полагаетесь на рантайм, который сам управляет памятью. В большинстве случаев это происходит незаметно, однако некоторые паттерны — например, объекты, ссылающиеся друг на друга по кругу, долгоживущие кэши или списки подписчиков — при неосторожном использовании могут приводить к утечкам памяти.
Причина в том, что Python по умолчанию создаёт сильные ссылки на объекты. Это означает, что объект будет оставаться в памяти до тех пор, пока в программе существует хотя бы одна такая сильная ссылка. В циклических структурах данных или в кэшах эти ссылки могут без необходимости откладывать освобождение объектов.
Слабые ссылки позволяют ссылаться на объекты, не мешая сборщику мусора удалять их. С их помощью можно создавать кэши, которые очищаются автоматически, списки подписчиков, которые сами избавляются от «мертвых» элементов, и другие структуры данных, не продлевающие жизнь объектам случайным образом.
В этой статье мы разберём, что такое слабые ссылки, зачем они нужны и как использовать их в Python. Мы начнём с обзора подсчёта ссылок, рассмотрим его ограничения, а затем углубимся в тему слабых ссылок и их практического применения.
Обзор подсчёта ссылок
Многие языки либо используют подсчёт ссылок как механизм управления памятью рантайма, либо предоставляют примитивы первого класса для работы с ним.
В этой модели у каждого объекта есть связанный с ним счётчик ссылок — количество мест, где он используется. Например, когда вы создаёте объект и присваиваете его переменной, его счётчик ссылок равен 1. Если вы присваиваете его другой переменной или передаёте в другую функцию, счётчик ссылок увеличивается на 1.
Аналогично, когда переменная выходит из области видимости или функция завершает выполнение, счётчик ссылок уменьшается. Если счётчик ссылок объекта достигает 0, объект освобождается или удаляется сборщиком мусора.
CPython использует подсчёт ссылок для управления памятью во время выполнения. Однако этот подход встречается и в других языках. Например, в C++ или Rust при использовании умных указателей под капотом применяется подсчёт ссылок: компилятор генерирует код, который увеличивает и уменьшает счётчик ссылок объектов.
Если вам интересно, как именно CPython реализует подсчёт ссылок на внутреннем уровне, вы можете ознакомиться с моей статьёй на эту тему.
Ограничения подсчёта ссылок
Подсчёт ссылок хорошо работает в большинстве случаев, но это не универсальное решение. За простоту приходится платить, и понимание этих ограничений помогает объяснить, зачем в Python существуют слабые ссылки.
Одно из таких ограничений — циклические ссылки. Они возникают, когда объекты ссылаются друг на друга по кругу, например в графовых структурах данных. Однако в сложных системах циклы могут появляться и случайно. В подобных ситуациях объекты, входящие в цикл, никогда не будут освобождены, пока этот цикл не будет разорван. Именно поэтому в CPython также реализован сборщик мусора для разрыва циклов (GC), который периодически запускается, сканирует объекты на наличие циклов и, если обнаруживает циклы, на которые больше нет внешних ссылок, разрывает их, позволяя освободить память.
Циклические ссылки могут негативно влиять на производительность, поскольку потребление памяти остаётся высоким до тех пор, пока не отработает GC, а само сканирование GC может быть дорогостоящим (в зависимости от количества объектов, которые ему нужно проверить).
Разберём это на примере. Рассмотрим следующий код.

Разложим всё по шагам:
Класс
MyNodeреализует узел связного списка с полемnext.print_node_objects— это вспомогательная функция. Она находит все объекты типаMyNode, которые в данный момент живы, и выводит их рефереры, то есть то, что удерживает на них ссылки.Для этого она использует
gc.get_objects(), чтобы получить список всех объектов, которые в данный момент существуют в интерпретаторе Python, и затем фильтрует его, оставляя только объекты типаMyNode.Рефереры объекта находятся с помощью метода
gc.get_referrers(), который возвращает список объектов-рефереров. Мы фильтруем этот список по типу, потому что во время вызова сам модуль gc тоже становится реферером, а нам нужно исключить его из результата.
В функции
main()мы вызываем функциюtest1(), которая создаёт два объектаMyNode, печатает их счётчики ссылок и завершается. После возврата изtest1()мы вызываемprint_node_objects(), чтобы проверить, остались ли в памяти какие-либо объекты типаMyNode.
Если запустить эту программу, вы увидите примерно такой вывод:
➜ uv run --python 3.13 -- cycles.py
n1 refcount: 2
n2 refcount: 2
n1 is being deleted
n2 is being deleted
No MyNode objects foundВ целом это ожидаемый результат, но давайте остановимся на нём подробнее, чтобы ничего не упустить.
Мы видим, что счётчик ссылок и у
n1, и уn2равен 2. Можно было бы ожидать значение 1, но на самом деле оно равно 2, потому что во время вызоваsys.getrefcountсчётчик ссылок объекта временно увеличивается.Мы также видим, что у обоих объектов вызывается метод
del, который печатает сообщение. Это происходит потому, чтоn1иn2— локальные переменные внутриtest1(), и когда функция завершается, её стековый фрейм уничтожается. В результате счётчики ссылок всех локальных объектов (параметров и локально созданных переменных) уменьшаются. В данном случаеn1иn2достигают счётчика ссылок0, поэтому они освобождаются, и вызывается их методdel.Наконец, в
main(), при вызовеprint_node_objects(), мы видим, что она не находит в куче ни одного живого объекта типаMyNode.
Далее мы можем провести ещё один тест: создать цикл между n1 и n2 и убедиться, что после возврата из тестовой функции объекты остаются живыми. На следующем рисунке показан обновлённый код, в котором я добавил новую функцию test2() и вызываю её из main().

Если запустить эту программу, мы увидим следующий вывод.
➜ uv run --python 3.13 -- cycles.py
n1 refcount: 2
n2 refcount: 2
n1 is being deleted
n2 is being deleted
No MyNode objects found
---------------------
n1 refcount: 3
n2 refcount: 3
n1 exists with referrers: [’n2’]
n2 exists with referrers: [’n1’]
n1 is being deleted
n2 is being deletedСосредоточимся на выводе после вызова test2().
Мы видим, что в
test2()счётчик ссылок дляn1иn2равен 3 — на единицу больше, чем вtest1(). Это происходит потому, чтоn1.nextсоздаёт ссылку наn2, аn2.next— ссылку наn1.Также видно, что после возврата из
test2()методdelуn1иn2не вызывается. Это означает, что объекты не освобождаются и остаются в памяти. Так происходит потому, что при выходе из функции интерпретатор уменьшает их счётчики ссылок, но на этот раз они не достигают нуля.После возврата из
test2(), когда мы вызываемprint_node_objects(), она сообщает, что объектыMyNode, созданные дляn1иn2, всё ещё живы. Мы также можем увидеть, что они живы именно потому, что удерживают циклическую ссылку друг на друга.В конечном итоге
n1иn2уничтожаются при завершении программы, поскольку перед остановкой CPython интерпретатор запускает сборщик мусора.
Чтобы такие циклические ссылки не приводили к утечкам памяти, в CPython есть сборщик мусора, который периодически запускается, обнаруживает циклы, на которые больше нет внешних ссылок, и разрывает их, позволяя освободить объекты, входящие в цикл. Вы можете убедиться в этом сами, добавив вызов gc.collect() сразу после вызова test2() в приведённой выше программе.
Если вы хотите понять, как сборщик мусора CPython обнаруживает и разрывает циклы, прочитайте мою статью о его внутреннем устройстве.
Однако существуют и другие способы избежать подобных ловушек подсчёта ссылок, и слабые ссылки — один из них. Давайте разберёмся, что это такое и как они работают.
Понимание слабых ссылок
Слабые ссылки находятся на противоположном конце спектра по сравнению с сильными ссылками. Слабая ссылка не увеличивает счётчик ссылок базового объекта, поэтому позволяет работать с объектом, не продлевая его время жизни.
Когда счётчик ссылок объекта опускается до 0, он может быть освобождён, даже если на него всё ещё существуют слабые ссылки. Естественно, это означает, что при использовании слабой ссылки всегда нужно проверять, жив ли базовый объект.
В Python для создания слабых ссылок используется функция weakref.ref() из модуля weakref. В неё передаётся объект, для которого нужно создать слабую ссылку. Например:
n1_weakref = weakref.ref(n1)Функция weakref.ref() создаёт слабую ссылку на указанный объект и возвращает вызываемый объект. Чтобы получить доступ к базовому объекту, этот объект нужно вызывать каждый раз. Если объект всё ещё жив, вызов вернёт ссылку на него, в противном случае — None. Например:
if n1_weakref():
print(f"name: {n1_weakref().name}")
else:
print("n1 no longer exists")На следующем рисунке показан полный пример создания слабой ссылки и обращения к ней в нашем работающем примере со связным списком.

Вывод:
➜ uv run --python 3.13 -- weakref_cycles.py
n1 refcount: 2
n1 refcount: 2
n1’s name: n1
n1 is being deleted
n1 no longer exists
---------------------
No MyNode objects foundПо выводу можно подтвердить несколько вещей:
Создание слабой ссылки не увеличивает счётчик ссылок объекта.
Слабая ссылка не мешает объекту быть освобождённым, когда его счётчик ссылок падает до 0 (в примере мы удалили
n1, и после этого уже не смогли обратиться к нему через слабую ссылку).
Задачу исправления циклической ссылки, которую мы создали в test2(), я оставляю вам в качестве упражнения.
Другие сценарии использования слабых ссылок
До этого мы рассматривали слабые ссылки как средство борьбы с циклами, но их польза этим не ограничивается. Модуль weakref также предоставляет готовые контейнеры, построенные поверх слабых ссылок. Эти контейнеры — WeakValueDictionary и WeakSet — помогают управлять вспомогательными структурами данных, которые не должны продлевать время жизни своих элементов. Они решают практические задачи вроде кэширования, реестров и списков подписчиков, где автоматическая очистка не просто удобна, а критически важна для предотвращения утечек памяти.
WeakValueDictionary
Модуль weakref предоставляет WeakValueDictionary — структуру, которая выглядит и ведёт себя как обычный словарь, но с важным отличием: его значения хранятся только через слабые ссылки. Если на значение больше нигде не остаётся сильных ссылок, соответствующая запись автоматически исчезает из словаря.
Это делает WeakValueDictionary естественным выбором для кэшей и мемоизации. Представьте, что вы вычисляете дорогие по ресурсам результаты или загружаете крупные структуры данных и хотите переиспользовать их, пока они находятся в памяти. При этом вы не хотите, чтобы сам кэш удерживал их навсегда. WeakValueDictionary как раз обеспечивает этот баланс: он хранит результаты ровно столько, сколько они нужны остальной программе.
Ещё один классический сценарий — интернирование объектов или реестры. Например, вам может понадобиться гарантировать, что для некоторого ресурса существует ровно один канонический объект (будь то запись в таблице символов, соединение с базой данных или разобранная схема). Используя WeakValueDictionary, вы избегаете искусственного продления времени жизни таких ресурсов.
Вот простой пример:
import weakref
class Data:
def init(self, name):
self.name = name
def repr(self):
return f"Data({self.name})"
cache = weakref.WeakValueDictionary()
obj = Data("expensive_result")
cache["key"] = obj
print("Before deletion:", dict(cache))
# Drop the strong reference
obj = None
print("After deletion:", dict(cache))Вывод:
Before deletion: {'key': Data(expensive_result)}
After deletion: {}Обратите внимание, как запись в кэше исчезает автоматически, как только пропадает последняя сильная ссылка. Никакой ручной очистки не требуется. Под капотом это реализовано с помощью callback’ов weakref — того же механизма, который мы увидим в разделе про callbacks.
WeakSet
Ещё один контейнер из модуля weakref — это WeakSet. Он похож на обычный set, но хранит слабые ссылки на свои элементы. Если объект удаляется сборщиком мусора, он автоматически исчезает из множества.
Один из особенно удобных сценариев — отслеживание подписчиков, наблюдателей или слушателей. Это объекты, которые регистрируются для получения событий от другого объекта (часто называемого издателем). Например:
GUI-фреймворки: виджеты слушают события вроде смены темы или изменения размеров окна.
Шины событий: сервисы подписываются на события логирования, метрики или доменные события.
Плагинные системы: плагины регистрируют callbacks при загрузке, чтобы реагировать на хуки.
Фоновые сервисы: временные сессии (например, WebSocket-соединения) слушают обновления от долгоживущего менеджера.
Во всех этих случаях подписчики часто живут недолго, тогда как издатель существует значительно дольше. Использование обычного set для хранения подписчиков чревато утечками памяти, поскольку сильная ссылка в множестве будет удерживать подписчика в памяти даже тогда, когда остальная программа о нём уже «забыла». С WeakSet сборщик мусора автоматически удаляет подписчиков, на которые больше нет сильных ссылок, поэтому вам не нужно реализовывать явную логику отписки для каждого пути завершения.
Вот простой пример:
import weakref
class Listener:
def init(self, name):
self.name = name
def repr(self):
return f"Listener({self.name})"
listeners = weakref.WeakSet()
l1 = Listener("A")
l2 = Listener("B")
listeners.add(l1)
listeners.add(l2)
print("Before deletion:", list(listeners))
# Remove one listener
l1 = None
import gc; gc.collect()
print("After deletion:", list(listeners))Вывод:
Before deletion: [Listener(A), Listener(B)]
After deletion: [Listener(B)]Этот паттерн часто развивается в полноценную модель «издатель–подписчик»:
class Publisher:
def init(self):
self._subs = weakref.WeakSet()
def subscribe(self, sub):
self._subs.add(sub)
def notify(self, payload):
for s in list(self._subs):
s.handle(payload)
class Subscriber:
def init(self, name):
self.name = name
def handle(self, payload):
print(self.name, "got:", payload)
pub = Publisher()
sub = Subscriber("one")
pub.subscribe(sub)
pub.notify({"event": 1}) # delivered
sub = None # drop last strong ref
import gc; gc.collect()
pub.notify({"event": 2}) # nothing printed; WeakSet cleaned itselfИспользование WeakSet в этом случае предотвращает утечки памяти и упрощает управление жизненным циклом объектов. Единственное ограничение — в WeakSet можно добавлять только объекты, поддерживающие слабые ссылки (то есть, как правило, пользовательские классы). Встроенные типы вроде int или tuple не подойдут. Если ваш класс использует slots, необходимо включить weakref, чтобы разрешить создание слабых ссылок.
Callbacks у слабых ссылок
Ещё одна полезная возможность weakref.ref — это привязка callback’а. Callback — это функция, которая автоматически вызывается в момент, когда объект, на который указывает слабая ссылка, готовится к финализации. Это удобно, если нужно очистить вспомогательные структуры данных или освободить ресурсы, когда объект исчезает.
import weakref
class Resource:
def init(self, name):
self.name = name
def repr(self):
return f"Resource({self.name})"
def on_finalize(wr):
print("Resource has been garbage collected:", wr)
obj = Resource("temp")
wr = weakref.ref(obj, on_finalize)
print("Created weak reference:", wr)
# Drop strong reference
obj = None
# Force GC for demo purposes
import gc; gc.collect()Вывод:
Created weak reference: <weakref at 0x75f6773870b0; to ‘Resource’ at 0x75f677c4ee40>
Resource has been garbage collected: <weakref at 0x75f6773870b0; dead>В этом примере callback on_finalize вызывается в тот момент, когда экземпляр Resource готовится к сборке мусора. После этого сама слабая ссылка переходит в «мёртвое» состояние. Такой паттерн полезен, когда вам нужно реализовать собственную логику очистки, привязанную к жизненному циклу объекта.
Стоит также отметить, что контейнеры вроде WeakValueDictionary и WeakSet используют тот же самый механизм внутри себя: они прикрепляют callbacks к своим слабым ссылкам, благодаря чему записи автоматически удаляются, когда соответствующие объекты финализируются.
Заключение
Слабые ссылки — это не тот инструмент, к которому вы будете обращаться каждый день, но в нужный момент они решают вполне реальные проблемы. На базовом уровне weakref.ref позволяет ссылаться на объект, не влияя на время его жизни, а также даёт возможность привязать callback, который выполнит код очистки в момент, когда объект будет собран. Поверх этого примитива Python предоставляет WeakValueDictionary и WeakSet — контейнеры более высокого уровня для кэшей, реестров и списков подписчиков, которые автоматически очищаются, когда их содержимое исчезает.
Подытожим различия:

В совокупности эти возможности позволяют строить системы, бережно относящиеся к памяти: избегать утечек, сокращать объём вспомогательной логики и уважать естественные жизненные циклы объектов. Понимание слабых ссылок и умение применять их в нужных местах поможет вам писать код, который будет и безопаснее, и эффективнее.
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!
