ЗАМЕР
bench/closure-lifetime/retention.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/closures-and-scope
- Как запустить
## Версии **Вывод одинаков на 3.11.15, 3.12.3, 3.13.7 и 3.14.7** — различается только строка `PY`. Различий по версиям нет. ## Прогон
Запись прогона
Замеры: замыкание и время жизни объекта
Один сюжет, которого нет в bench/closures/: замыкание держит объект живым
дольше, чем ожидает вызывающий. Проверяется через weakref — жив объект или
мёртв, — и через gc. Ни одного числа времени и ни одного числа байт здесь
нет: удержание — факт двоичный, и меряется он двоичным ответом.
| скрипт | что показывает |
|---|---|
retention.py |
ячейка держит только упомянутые имена; одна ячейка на две функции; обработчик в реестре держит владельца; цикл через ячейку требует сборщика; отпускание присваиванием None |
Запуск:
for v in 3.11 3.12 3.13 3.14; do
echo "== $v"; python$v bench/closure-lifetime/retention.py
done
Версии
Вывод одинаков на 3.11.15, 3.12.3, 3.13.7 и 3.14.7 — различается только
строка PY. Различий по версиям нет.
Прогон
$ python3.13 retention.py
PY 3.13.7
1) локальная переменная без замыкания после выхода из функции: мёртв
2) после выхода из make_callback:
упомянутый в теле data : жив
неупомянутый unused : мёртв
__closure__ замыкания : 1 ячейка(и)
co_freevars : ('data',)
компилятор кладёт в ячейку только те имена, что реально читаются в теле
3) после del cb: мёртв
4) две функции делят одну ячейку:
объект: жив
reader.__closure__[0] is writer.__closure__[0]: True
после del reader (остался writer): жив
после del writer: мёртв
5) соединение отпущено вызывающим, обработчик в реестре: жив
что держит: ('self',)
после очистки реестра: мёртв
6) цикл «узел -> обработчик -> ячейка -> узел», сборщик выключен: жив
после gc.collect(): мёртв | собрано объектов: True
счётчик ссылок такой цикл не разрывает — нужен именно сборщик циклов
7) до release: жив | cb2() = отпускаемый
после release(): мёртв | cb2() = отпущено
ячейка осталась, но теперь в ней None — объект отпущен без удаления функции
Что здесь читать
Пункт 1 против пункта 2 — вся суть за две строки. Обычная локальная переменная умирает на выходе из функции; та же переменная, упомянутая во вложенной функции, переживает выход ровно настолько, насколько живёт эта вложенная функция. Пункт 3 закрывает круг: удаляем функцию — умирает объект.
Пункт 2, вторая строка, снимает частое преувеличение: замыкание держит не
весь кадр, а только те имена, которые компилятор увидел в теле. unused
мёртв, co_freevars — ровно ('data',).
Пункт 5 — тот самый практический случай, ради которого пункт вообще нужен.
self попадает в co_freevars обработчика, обработчик лежит в глобальном
списке, и del conn не делает ничего. Ни ошибки, ни предупреждения: объект
просто не умирает, пока живёт подписка.
Пункт 6 — цикл «объект → его обработчик → ячейка → объект». Со счётчиком
ссылок он не разбирается; при gc.disable() объект остаётся жив, gc.collect()
его забирает. Это ровно та ситуация, в которой отложенная память выглядит
утечкой.
Пункт 7 — как отпустить, не удаляя функцию: nonlocal data; data = None.
Ячейка остаётся, а объект в ней — нет.
Первоисточники
Механизм — Doc/reference/executionmodel.rst (тег v3.14.5), раздел
«Resolution of names»: свободное имя разрешается в момент выполнения через
ячейку объемлющей области. Соответствующая проверка запуском уже есть в
bench/closures/ и в уроке про замыкания; здесь она не повторяется.
Что именно попадает в ячейку — Doc/reference/datamodel.rst, строки 1528–1534
(тег v3.14.5):
codeobject.co_freevars— Atuplecontaining the names of free (closure) variables that a nested scope references in an outer scope. See alsofunction.__closure__. Note: references to global and builtin names are not included.
Перевод: «Кортеж, содержащий имена свободных переменных (переменных
замыкания), на которые вложенная область видимости ссылается во внешней
области. См. также function.__closure__. Замечание: ссылки на глобальные и
встроенные имена сюда не входят.» Пункт 2 замера печатает именно этот кортеж
и сопоставляет его с тем, какой объект остался жив.
И там же, строки 592–597, про хранилище:
function.__closure__—Noneor atupleof cells that contain bindings for the names specified in theco_freevarsattribute of the function's code object. A cell object has the attributecell_contents.
Перевод: «None либо кортеж ячеек, содержащих связывания для имён, указанных в
атрибуте co_freevars объекта кода функции. У объекта-ячейки есть атрибут
cell_contents.» Пункт 7 замера меняет объект именно через это связывание.
weakref как инструмент проверки — Doc/library/weakref.rst, строки 27–31:
A weak reference to an object is not enough to keep the object alive: when the only remaining references to a referent are weak references, garbage collection is free to destroy the referent and reuse its memory for something else.
Перевод: «Слабой ссылки на объект недостаточно, чтобы объект остался жив: когда
единственные оставшиеся ссылки на объект — слабые, сборка мусора вправе
уничтожить объект и переиспользовать его память под что-то другое.» Поэтому
ref() is not None — корректный ответ на вопрос «держит ли его ещё кто-нибудь
по-настоящему».
Оговорка про версию
3.14 здесь — это 3.14.7. Вывод на ней совпадает с 3.13.7 построчно.
Пункт 6 зависит от поведения сборщика циклов, и от rc до финала оно меняться не
должно — но проверено именно на rc.
Скрипт
184 строк"""Замыкание держит объект живым: где именно рвётся ожидаемое время жизни.
Один сюжет: функция, определённая внутри другой, попадает в `__closure__` со
всеми свободными именами — и через них удерживает объекты, о которых
вызывающий уже забыл. Проверяется через weakref (жив/мёртв), а не через время
и не через размер: числа времени здесь не нужны и не приводятся.
Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import gc
import sys
import weakref
print("PY", sys.version.split()[0])
print()
class Big:
"""Объект, за жизнью которого следим."""
def __init__(self, tag):
self.tag = tag
self.payload = bytearray(1024)
def __repr__(self):
return f"Big({self.tag!r})"
def alive(ref):
return "жив" if ref() is not None else "мёртв"
# --- 1. Контроль: без замыкания объект умирает сразу -----------------------
def no_closure():
obj = Big("контроль")
return weakref.ref(obj)
r = no_closure()
gc.collect()
print("1) локальная переменная без замыкания после выхода из функции:", alive(r))
print()
# --- 2. Замыкание удерживает объект, даже если функция его не использует ---
def make_callback():
data = Big("данные")
unused = Big("не используется") # noqa: F841
def callback():
return data.tag # упомянут только data
return callback, weakref.ref(data), weakref.ref(unused)
cb, r_data, r_unused = make_callback()
gc.collect()
print("2) после выхода из make_callback:")
print(" упомянутый в теле data :", alive(r_data))
print(" неупомянутый unused :", alive(r_unused))
print(" __closure__ замыкания :", len(cb.__closure__ or ()), "ячейка(и)")
print(" co_freevars :", cb.__code__.co_freevars)
print(" компилятор кладёт в ячейку только те имена, что реально читаются в теле")
print()
# --- 3. Пока жива функция — жив объект -------------------------------------
del cb
gc.collect()
print("3) после del cb:", alive(r_data))
print()
# --- 4. Тот же объект удерживается через ЛЮБОЕ имя в области --------------
def make_two():
payload = Big("общий")
def reader():
return payload.tag
def writer(v):
nonlocal payload
payload = v
return reader, writer, weakref.ref(payload)
reader, writer, r_shared = make_two()
gc.collect()
print("4) две функции делят одну ячейку:")
print(" объект:", alive(r_shared))
print(" reader.__closure__[0] is writer.__closure__[0]:",
reader.__closure__[0] is writer.__closure__[0])
del reader
gc.collect()
print(" после del reader (остался writer):", alive(r_shared))
del writer
gc.collect()
print(" после del writer:", alive(r_shared))
print()
# --- 5. Практический случай: обработчик, переживший «владельца» ------------
registry = []
class Connection:
def __init__(self, tag):
self.tag = tag
self.buffer = bytearray(4096)
def subscribe(self):
# ЛОВУШКА: замыкание над self — соединение не умрёт, пока
# обработчик лежит в глобальном registry.
def on_event(event):
return f"{self.tag}:{event}"
registry.append(on_event)
conn = Connection("conn-1")
r_conn = weakref.ref(conn)
conn.subscribe()
del conn
gc.collect()
print("5) соединение отпущено вызывающим, обработчик в реестре:", alive(r_conn))
print(" что держит:", registry[0].__code__.co_freevars)
registry.clear()
gc.collect()
print(" после очистки реестра:", alive(r_conn))
print()
# --- 6. Цикл через замыкание: обычный refcount не справляется --------------
class Node:
def __init__(self, tag):
self.tag = tag
self.handler = None
def make_cycle():
node = Node("узел")
def handler():
return node.tag # замыкание ссылается на node
node.handler = handler # node ссылается на замыкание -> цикл
return weakref.ref(node)
gc.disable()
r_cycle = make_cycle()
print("6) цикл «узел -> обработчик -> ячейка -> узел», сборщик выключен:",
alive(r_cycle))
gc.enable()
collected = gc.collect()
print(" после gc.collect():", alive(r_cycle), "| собрано объектов:", collected > 0)
print(" счётчик ссылок такой цикл не разрывает — нужен именно сборщик циклов")
print()
# --- 7. Как разорвать удержание намеренно ----------------------------------
def make_droppable():
data = Big("отпускаемый")
def callback():
if data is None:
return "отпущено"
return data.tag
def release():
nonlocal data
data = None
return callback, release, weakref.ref(data)
cb2, release, r_drop = make_droppable()
gc.collect()
print("7) до release:", alive(r_drop), "| cb2() =", cb2())
release()
gc.collect()
print(" после release():", alive(r_drop), "| cb2() =", cb2())
print(" ячейка осталась, но теперь в ней None — объект отпущен без удаления функции")