PythonMiddleTechnical
Что такое циклические ссылки и как Python их собирает?
Циклические ссылки — когда объекты держат друг друга, а внешних ссылок нет. Refcount их не освободит; cyclic GC (модуль gc, generational, по поколениям 0/1/2) находит недостижимые группы и удаляет. Используйте weakref для parent-back-refs.
Что это
CPython управляет памятью двумя механизмами: reference counting (основной) и cyclic garbage collector (модуль gc) для случаев, которые refcount не вытянет. Циклическая ссылка возникает, когда объекты A и B держат друг друга (напрямую или через цепочку), и из программы на них уже нет ссылок. Refcount каждого больше нуля — refcount не освободит.
Как работает cyclic GC
- GC отслеживает только container-объекты (списки, словари, классы, замыкания). Числа, строки, tuple из неконтейнеров не трекаются.
- Generational: 3 поколения (0, 1, 2). Новые объекты идут в gen 0, выжившие — в старшие. Пороги:
gc.get_threshold() == (700, 10, 10). - Алгоритм Колдер-Бэкона: GC временно вычитает «внутренние» ссылки внутри подозрительного подграфа и смотрит, остался ли у кого-нибудь refcount > 0. Если нет — группа недостижима, она освобождается.
Пример
import gc
import weakref
class Node:
def __init__(self, name: str):
self.name = name
self.next: Node | None = None
def __del__(self):
print(f"del {self.name}")
# Создаём цикл: a -> b -> a
a = Node("A")
b = Node("B")
a.next = b
b.next = a
# refcount A и B по 2 (внешняя ссылка + ссылка из соседа)
# del убирает внешние, остаётся цикл
del a, b
print("collected:", gc.collect()) # collected: 2 + сообщения del A / del B
# Решение: weakref для обратной ссылки
class Parent:
def __init__(self):
self.children: list[Child] = []
class Child:
def __init__(self, parent: Parent):
self.parent = weakref.ref(parent) # не удерживает parent
p = Parent()
c = Child(p)
p.children.append(c)
del p
# parent умирает сразу по refcount = 0, без gc.collect()
print(c.parent()) # None
Управление GC
gc.collect()— форсированный проход (можно по поколению).gc.get_stats(),gc.get_objects(),gc.get_referrers(x)— диагностика.gc.disable()в hot path и потомgc.enable()— иногда полезно (например, при массовой загрузке данных), но осторожно.gc.set_debug(gc.DEBUG_LEAK)печатает несобираемые объекты.
Подводные камни
- Двусторонние parent/child без
weakref— типичный источник большого RSS у Django/SQLAlchemy сессий. - Замыкания в декораторах удерживают захваченные объекты, образуя циклы через
self. - Раньше (до Python 3.4)
__del__у объектов в цикле приводил кgc.garbage— цикл не собирался. С PEP 442 это исправлено, но__del__с побочными эффектами всё равно плохая идея. - Внешние ресурсы (файлы, сокеты, БД-соединения) лучше закрывать через
with/contextlib.closing— не полагаться на GC. - Путать цикл и обычную утечку через глобальный список/кеш — GC не поможет, нужен
weakref.WeakValueDictionaryили явныйevict.
Common mistakes
- Не показывать пример A -> B -> A.
- Говорить, что refcount решает все случаи.
- Не знать про weakref как инструмент.
What the interviewer is testing
- Объясняет недостижимость извне.
- Понимает роль gc.
- Отличает цикл от удерживаемой ссылки.