Как работает счетчик ссылок в Python
Что проверяют
Понимаете ли, что счётчик ссылок это деталь реализации CPython, а не свойство языка. И знаете ли, что он в принципе не разбирает циклы, для которых существует отдельный сборщик.
Как отвечать
Механика. У каждого объекта в CPython есть поле счётчика. Он растёт при появлении новой ссылки: присваивание имени, добавление в список или словарь, передача аргументом, захват замыканием. Он падает при del, переприсваивании имени, выходе из области видимости, удалении контейнера.
Ноль означает немедленное освобождение: вызывается __del__, память возвращается аллокатору. Это отличие от JVM или Go, где момент смерти объекта неопределён. Правда, полагаться на это для закрытия ресурсов всё равно нельзя, для файлов и соединений есть with.
import sys
a = [1, 2, 3]
b = a
print(sys.getrefcount(a)) # на единицу больше ожидаемого
del b # счётчик уменьшилсяsys.getrefcount всегда возвращает на единицу больше, потому что сам аргумент функции тоже является ссылкой. У малых чисел и коротких строк значения выглядят огромными из-за интернирования: эти объекты общие для всего процесса.
Циклы счётчик не решает. Если a.ref = b и b.ref = a, то после удаления внешних имён у обоих остаётся по единице, и память не освобождается. Для этого есть отдельный сборщик модуля gc:
- он следит только за контейнерными объектами (списки, словари, множества, экземпляры классов), потому что только они могут образовать цикл;
- работает алгоритмом mark and sweep по трём поколениям, свежие объекты проверяются чаще;
- запускается по порогам числа выделений, а не по расписанию, и его можно отключить
gc.disable()или вызвать вручнуюgc.collect().
Строки и числа в поле зрения gc не попадают вовсе, их освобождает только счётчик.
Актуальная деталь. С Python 3.4 (PEP 442) объекты с __del__ внутри цикла собираются нормально. Утверждение «__del__ навсегда подвешивает цикл» устарело, хотя до сих пор встречается в старых статьях.
Плюсы и минусы. Плюсы: простота, предсказуемость, освобождение сразу после последней ссылки, ровная нагрузка без пауз. Минусы: накладные расходы на каждое присваивание, память под само поле счётчика, невозможность разобрать цикл и требование атомарности изменений, что исторически было одной из причин существования GIL. В свободных от GIL сборках 3.13+ схема подсчёта переработана.
Реализация, а не язык. PyPy и Jython счётчиком не пользуются вовсе, там трассирующий сборщик, и объекты умирают позже. Код, завязанный на мгновенное освобождение, там ломается.
Где спотыкаются
Считают подсчёт ссылок частью спецификации языка и говорят, что сборщик мусора занимается всеми объектами. gc подключается только к циклам среди контейнеров, всё остальное освобождает счётчик, и эту границу интервьюер обычно и уточняет.