Три сценария по шагам: где счётчика достаточно, где он бессилен принципиально и как выглядит настоящая утечка.
Счётчик ссылок описывается одной фразой — «объект живёт, пока на него смотрят» — и из этой фразы выводится и работающий with, и невозможность собрать цикл, и половина утечек в продакшене. Ниже эта фраза раскручена по шагам. Число в кружке — значение ob_refcnt, то самое поле из первой лаборатории.
Второй сценарий воспроизводится целиком и показывает то же самое числами. Вставляется как есть:
import gc, sys
class Node:
def __init__(self, name): self.name, self.next = name, None
def __repr__(self): return f"Node({self.name!r})"
a, b = Node("A"), Node("B")
a.next, b.next = b, a
print("rc(A):", sys.getrefcount(a) - 1) # -1: аргумент getrefcount тоже ссылка
# → rc(A): 2
gc.collect() # чистим, чтобы счёт был честным
tracked_before = len(gc.get_objects())
del a, b # имён нет, объекты недостижимы
print("собрано счётчиком:", len(gc.get_objects()) - tracked_before)
# → собрано счётчиком: 0 ← ни один не умер
print("собрано обходом:", gc.collect())
# → собрано обходом: 2
lru_cache на методе. Объект достижим, значит, он не мусор — с точки зрения интерпретатора всё правильно.sys.getrefcount минус единица за собственный аргумент. Порядок разрушения в третьем сценарии — тоже настоящий: CPython разбирает цепочку от внешнего объекта к внутреннему, потому что каждое уничтожение снимает следующую ссылку.