← ООП вглубь · 🏠 Домой · Итератор →
Коротко. Основной механизм — подсчёт ссылок: у каждого объекта есть счётчик, и как только он падает до нуля, память освобождается немедленно, без пауз на сборку мусора.
import sys
a = []
sys.getrefcount(a) # 2
b = a
sys.getrefcount(a) # 3
del b
sys.getrefcount(a) # 2Подвох. Почему 2, а не 1? Потому что аргумент, переданный в
getrefcount, — это ещё одна временная ссылка. Значение всегда на единицу
больше «настоящего», и абсолютные числа тут смысла не имеют — важна динамика.
Глубже. Детерминированность — сильная сторона схемы: файл, у которого
пропала последняя ссылка, закрывается сразу, поэтому в CPython работает
open(...).read() без явного закрытия. Но это деталь реализации: в PyPy
и Jython сборка трассирующая, и там тот же код держит дескриптор открытым до
следующей сборки. Поэтому ресурсы всё равно закрывают явно — через
контекстный менеджер.
С Python 3.12 (PEP 683) часть объектов «бессмертны»: None, True, False,
маленькие числа получили фиксированный счётчик, который не меняется, —
это убирает запись в общую память при каждом обращении и было необходимо
для free-threading.
Коротко. Подсчёт ссылок не видит циклов: два объекта, ссылающиеся друг на
друга, держат счётчики ненулевыми, даже когда снаружи на них никто не
ссылается. Такие группы находит отдельный поколенческий сборщик (модуль gc).
flowchart LR
subgraph refcount["Подсчёт ссылок справляется"]
direction LR
name1["имя a"] --> obj1["список<br/>refcount = 1"]
name1 -.->|"del a → refcount = 0"| free1["память освобождена немедленно"]
end
flowchart LR
subgraph cycle["Цикл: refcount бессилен"]
direction LR
x["x"] -.->|"del x"| gone1["ссылки снаружи нет"]
y["y"] -.->|"del y"| gone2["ссылки снаружи нет"]
n1["Node 1<br/>refcount = 1"] --> n2["Node 2<br/>refcount = 1"]
n2 --> n1
n1 -.->|"находит только gc.collect()"| gcm["поколенческий сборщик"]
end
Поколения: новые объекты попадают в поколение 0, пережившие сборку переезжают дальше, и старшие поколения проверяются тем реже, чем они старше.
flowchart LR
new["Новый объект"] --> g0["Поколение 0<br/>проверяется часто"]
g0 -->|"пережил сборку"| g1["Поколение 1<br/>реже"]
g1 -->|"пережил сборку"| g2["Поколение 2<br/>совсем редко"]
g0 -.->|"мусор"| free["Освобождение"]
g1 -.-> free
g2 -.-> free
import gc
class Node:
def __init__(self): self.ref = None
x = Node(); y = Node()
x.ref = y; y.ref = x # цикл
del x, y # снаружи ссылок нет, но счётчики == 1
gc.collect()
# 2 — объекты освободил уже сборщик, а не refcountingСборщик отслеживает только контейнеры (объекты, которые могут ссылаться на
другие), проходит их поколениями 0/1/2: молодое поколение проверяется часто,
пережившие сборку объекты переводятся в старшее и проверяются реже.
Пороги видно в gc.get_threshold(): первое число — сколько выделений минус
освобождений накопится, прежде чем проверят поколение 0, второе и третье —
сколько сборок поколения 0 (и 1) пройдёт, прежде чем проверят следующее
поколение. Дефолт зависит от версии CPython: до 3.12 — (700, 10, 10),
начиная с 3.12 — (2000, 10, 0) (третий порог занулили из-за изменений
в поколенческом сборщике).
Подвох. Утечка в Python — обычно не «цикл», а живая ссылка, о которой забыли: глобальный кеш, список подписчиков, замыкание, залипшее в декораторе. Циклы сборщик разберёт сам, а вот такую ссылку — нет.
Глубже. gc.disable() иногда включают в короткоживущих CPU-задачах и
перед fork (сборщик трогает объекты и «пачкает» страницы, ломая
copy-on-write). Полностью выключать его в долгоживущем сервисе опасно —
циклы накопятся. Отладка: gc.set_debug(gc.DEBUG_LEAK), gc.garbage,
tracemalloc для снимков распределения памяти.
Коротко. weakref — ссылка, которая не увеличивает счётчик и не
мешает объекту умереть. После смерти объекта вызов слабой ссылки даёт None.
import weakref
class Big: ...
obj = Big()
r = weakref.ref(obj)
r() is obj # True
del obj
r() # NoneДва типовых применения:
- кеши, которые не должны удерживать значения:
weakref.WeakValueDictionary(запись исчезает, когда объект умирает) иWeakKeyDictionary— готовые словари на слабых ссылках; - обратные ссылки в структурах:
child.parentделают слабой, чтобы дерево разбиралось подсчётом ссылок, а не ждало сборщика циклов.
Подвох. Слабую ссылку поддерживает не всё: на list, dict, tuple,
int, str её не создать (TypeError: cannot create weak reference), а класс
со __slots__ теряет эту возможность, если не добавить "__weakref__"
в слоты (см. ООП вглубь).
Коротко. copy() создаёт новый контейнер, но кладёт в него те же самые
вложенные объекты (shallow). deepcopy() рекурсивно копирует всё дерево.
import copy
orig = [[1, 2], [3, 4]]
shallow = copy.copy(orig)
deep = copy.deepcopy(orig)
orig[0].append(99)
shallow[0] # [1, 2, 99] — тот же вложенный список
deep[0] # [1, 2] — независимая копияПодвох. Все привычные способы «скопировать список» — тоже поверхностные:
orig[:][0] is orig[0] # True
list(orig)[0] is orig[0] # True
orig.copy()[0] is orig[0] # TrueРовно из-за этого «копия» конфигурации со вложенным словарём продолжает меняться вместе с оригиналом.
Глубже. deepcopy ведёт memo-словарь уже скопированных объектов, поэтому
корректно обрабатывает циклы и общие подобъекты (два поля, указывавшие на один
список, в копии тоже будут указывать на один). Поведение настраивается
методами __copy__/__deepcopy__, а deepcopy дорог — на больших структурах
дешевле бывает пересобрать объект явно.
Коротко. is сравнивает идентичность (тот ли это самый объект),
== вызывает __eq__ и сравнивает значения. is уместен только для
синглтонов: None, True, False, объекты-часовые.
x = int("1000")
y = int("1000")
x == y # True
x is y # False — два разных объекта с одним значениемПодвох. Обратный случай коварнее: CPython переиспользует объекты — кеш
целых от -5 до 256, интернирование строковых литералов, общие константы
внутри одного блока кода. Поэтому написанные рядом x = 1000 и y = 1000
дадут x is y → True (обе ссылки на одну константу), а те же числа,
пришедшие из разных мест программы, — False. Код, случайно построенный на
is, «работает» ровно до этого момента. Подробнее — в
Типах данных.
Практическое правило: x is None — всегда, x == None — никогда;
для всего остального ==.