Ответ
Циклическая ссылка (reference cycle) — это ситуация в управлении памятью, когда два или более объекта ссылаются друг на друга, создавая замкнутый цикл сильных ссылок. Это мешает сборщику мусора освободить память, занимаемую этими объектами, что приводит к утечкам памяти.
Почему это проблема?
Стандартный механизм сборки мусора в Python (подсчёт ссылок) не может обработать циклы. Когда счётчик ссылок на объект падает до нуля, объект удаляется. В цикле счётчики ссылок объектов никогда не достигнут нуля, даже если на них больше нет ссылок извне цикла.
Хотя в Python есть дополнительный циклический сборщик мусора (cyclic GC), который находит и удаляет такие циклы, он работает периодически и не может разрешить циклы, если в них есть объекты с методом __del__
.
Пример создания цикла:
class Node:
def __init__(self, name):
self.name = name
self.parent = None
print(f"Node {self.name} created")
def __del__(self):
# Этот метод усложняет работу циклического GC
print(f"Node {self.name} destroyed")
# Создаем два объекта
a = Node('A')
b = Node('B')
# Создаем циклическую ссылку: a ссылается на b, а b на a
a.parent = b
b.parent = a
# Удаляем внешние ссылки на объекты
del a
del b
# Сообщение "destroyed" не будет выведено, объекты остались в памяти
Как бороться с циклическими ссылками?
Основной способ — использовать слабые ссылки с помощью модуля weakref
. Слабая ссылка не увеличивает счётчик ссылок объекта, что позволяет разорвать цикл.
Решение с помощью weakref
:
import weakref
class Node:
def __init__(self, name):
self.name = name
# Храним слабую ссылку на родителя
self._parent_ref = None
print(f"Node {self.name} created")
@property
def parent(self):
return self._parent_ref() if self._parent_ref else None
@parent.setter
def parent(self, parent_node):
self._parent_ref = weakref.ref(parent_node) if parent_node else None
def __del__(self):
print(f"Node {self.name} destroyed")
# Создаем объекты
a = Node('A')
b = Node('B')
# Создаем ссылки (одна из них слабая)
a.parent = b
b.parent = a # Эта ссылка будет слабой
del a
del b
# Теперь оба сообщения "destroyed" будут выведены, утечки памяти нет
Другой подход — явный разрыв цикла перед удалением объектов, например, вызвав b.parent = None
.