← Назад к вопросам

Что такое Retain Cycle?

2.3 Middle🔥 191 комментариев
#DevOps и инфраструктура#Django

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Retain Cycle (Циклические ссылки)

Retain Cycle — это проблема управления памятью, при которой два или более объекта взаимно ссылаются друг на друга, что предотвращает их удаление из памяти даже когда они больше не нужны. Это приводит к утечкам памяти.

Хотя Retain Cycle традиционно обсуждается в контексте Objective-C и Swift (где используется управление памятью Reference Counting), эта концепция релевантна и для Python при работе с циклами ссылок.

Как происходит утечка памяти

Пример 1: Циклическая ссылка в Python

class Person:
    def __init__(self, name):
        self.name = name
        self.friend = None

# Создаём двух людей
alice = Person("Alice")
bob = Person("Bob")

# Они ссылаются друг на друга (циклическая ссылка)
alice.friend = bob
bob.friend = alice

print(f"alice.friend.name = {alice.friend.name}")  # Bob
print(f"bob.friend.name = {bob.friend.name}")      # Alice

# Теперь удаляем переменные
del alice
del bob

# ПРОБЛЕМА: объекты в памяти остаются!
# Потому что alice.friend указывает на bob
# И bob.friend указывает на alice
# Никто не может их удалить
# Это утечка памяти

В CPython: это не критично благодаря garbage collector (сборщик мусора), который обнаруживает и удаляет циклы.

В других языках (JavaScript, Swift, Objective-C): утечка критична!

Визуализация проблемы

До удаления:
    alice  ──────┐
      │          │
      └─→ Person │
          friend ├──→ Bob ──┐
                 │          │
                 └─────────┬─┘
                          │
                    bob.friend = alice

После "del alice; del bob":
    Person(alice)  ←──┐
        friend ├──→ Person(bob)
                     friend ├──┘

    Никто не удерживает ссылку на эти объекты
    Но они ссылаются друг на друга
    Garbage Collector должен их очистить

Проблема в JavaScript (более критична)

В JavaScript до недавно не было автоматического garbage collection для циклов в старых версиях:

// JavaScript
class Person {
    constructor(name) {
        this.name = name;
        this.friend = null;
    }
}

let alice = new Person("Alice");
let bob = new Person("Bob");

alice.friend = bob;
bob.friend = alice;

// Удаляем ссылки
alice = null;
bob = null;

// В старых браузерах: утечка памяти!
// Объекты остаются в памяти
// Потому что интерпретатор не может разрешить цикл

// Решение: явно прерывать цикл
alice.friend = null;  // Прерываем цикл
bob.friend = null;

Проблема в Swift и Objective-C

В этих языках используется Reference Counting — счёт ссылок:

// Swift
class Person {
    var name: String
    var friend: Person?
    
    init(name: String) {
        self.name = name
    }
}

var alice: Person? = Person(name: "Alice")
var bob: Person? = Person(name: "Bob")

alice?.friend = bob  // Счётчик ссылок bob = 2
bob?.friend = alice  // Счётчик ссылок alice = 2

alice = nil  // Счётчик alice = 1 (bob.friend всё ещё ссылается)
bob = nil    // Счётчик bob = 1 (alice.friend всё ещё ссылается)

// УТЕЧКА: оба объекта остаются в памяти!
// Их счётчики никогда не станут 0

// Решение: использовать weak или unowned ссылки
class Person {
    var name: String
    weak var friend: Person?  // Weak — не увеличивает счётчик!
    
    init(name: String) {
        self.name = name
    }
}

Примеры утечек памяти в Python

Пример 1: Циклические ссылки с контейнерами

class Node:
    def __init__(self, value):
        self.value = value
        self.children = []  # Список потомков
        self.parent = None  # Ссылка на родителя

# Создаём дерево
root = Node("root")
child = Node("child")

# Циклическая ссылка
root.children.append(child)
child.parent = root  # ← Цикл: root → child → parent → root

del root
del child

# В CPython удалит объекты через garbage collector
# Но в других реализациях (Jython, IronPython) может быть утечка

Пример 2: Замыкания и self

class Button:
    def __init__(self, label):
        self.label = label
        self.callback = None
    
    def set_callback(self, callback):
        self.callback = callback

button = Button("Click me")

# Лямбда ссылается на button (self)
button.set_callback(lambda: print(button.label))

# Цикл: button → callback → button (замыкание захватило button)
# Garbage collector справится, но это плохая практика

# Решение: использовать weakref
import weakref

class Button:
    def __init__(self, label):
        self.label = label
        self.callback = None
    
    def set_callback(self, callback):
        self.callback = callback

button = Button("Click me")
button_ref = weakref.ref(button)

button.set_callback(lambda: print(button_ref().label) if button_ref() else None)
# Теперь button может быть удалён

Как обнаружить утечку памяти

import gc
import sys

class Leaky:
    def __init__(self):
        self.ref = self  # Циклическая ссылка на себя

# Отключим автоматический garbage collection
gc.disable()

# Создадим объекты с утечкой
objects = [Leaky() for _ in range(1000)]

# Удалим ссылки
del objects

# Объекты остаются в памяти
print(f"Объектов в памяти до gc: {len(gc.get_objects())}")

# Принудительно запустим сборщик мусора
gc.collect()

print(f"Объектов в памяти после gc: {len(gc.get_objects())}")

# Включим автоматический GC обратно
gc.enable()

Использование weakref (слабые ссылки)

weakref позволяет создавать ссылки, которые не препятствуют удалению объекта:

import weakref

class Person:
    def __init__(self, name):
        self.name = name
        self.friend = None

alice = Person("Alice")
bob = Person("Bob")

# Обычная сильная ссылка
alice.friend = bob  # bob останется в памяти, пока жива alice

# Слабая ссылка
alice.friend = weakref.ref(bob)  # bob может быть удалён
if alice.friend() is not None:  # Проверяем, жив ли объект
    print(alice.friend().name)

# Теперь bob может быть удалён
del bob
print(alice.friend())  # None (bob был удалён)

Лучшие практики

1. Избегай циклических ссылок

# ❌ Плохо
class A:
    def __init__(self):
        self.b = None

class B:
    def __init__(self):
        self.a = None  # Цикл!

# ✅ Хорошо
class A:
    def __init__(self, b):
        self.b = b  # B передаётся, нет цикла

2. Используй weakref когда нужна обратная ссылка

import weakref

class Parent:
    def __init__(self):
        self.children = []

class Child:
    def __init__(self, parent):
        self.parent = weakref.ref(parent)  # Слабая ссылка

3. Явно очищай циклы перед удалением

class Resource:
    def cleanup(self):
        self.circular_ref = None  # Разрываем цикл

r = Resource()
# ...
r.cleanup()
del r

Резюме

Retain Cycle — это циклическая ссылка между объектами, которая предотвращает их удаление из памяти:

  • В Python: обычно не проблема благодаря garbage collector, но может быть в других реализациях
  • В Swift/Objective-C: критична, решается weak/unowned ссылками
  • В JavaScript: может быть проблемой в старых браузерах
  • Решения: weakref, разрыв цикла вручную, избежание циклических ссылок при проектировании

Хороший разработчик должен понимать эту концепцию, даже если работает с Python.