Замыкания: захвачена переменная, а не значение — и цепочка областей проходит мимо тела класса
Замыкание держит ячейку, а не копию: значение читается в момент вызова, и одна ячейка достаётся всем внутренним функциям сразу. Привычная цепочка «локальная → объемлющая → глобальная → встроенные» умалчивает о двух вещах, на которых и ошибаются: тело класса в неё не входит, а присваивание где угодно в теле делает имя локальным для всего тела.
Полное техническое изложение
TL;DR
Функция, созданная внутри другой, продолжает пользоваться её переменными и после того, как внешняя функция завершилась. Это и есть замыкание. Важно, чем именно она пользуется: не копией значения, снятой в момент создания, а самой переменной — поэтому значение читается тогда, когда внутреннюю функцию вызывают, а не тогда, когда её создавали.
Отсюда главное следствие: захвачена переменная, а не значение, и расходится
это с ожиданием в трёх местах. Изменение переменной после создания функции до
неё доходит: v меняется на «второе» уже после def show, и show()
возвращает «второе». Доступ к переменной при этом общий: две внутренние
функции, читающие одно имя, ссылаются на один и тот же объект — проверено
сравнением по is. А привычная цепочка «локальная → объемлющая → глобальная →
встроенные» умалчивает о двух вещах: тело класса в неё не входит (метод,
читающий x, возьмёт x из объемлющей функции, даже если рядом в классе есть
свой) и присваивание где угодно в теле делает имя локальным для всего тела —
обращение к нему выше по коду даёт UnboundLocalError, не добираясь до
глобальной переменной с тем же именем.
Дальше — то, что отличает знающего от читавшего. Объект, через который идёт
общий доступ, называется ячейкой, а имя, которое функция использует, но не
определяет, — свободной переменной; справочник говорит про них прямо:
Name resolution of free variables occurs at runtime, not at compile time
(Разрешение имён свободных переменных происходит во время выполнения, а не во время компиляции).
Цена всего этого мала и при этом различима: обращение к ячейке стоит 9,1 нс
против 6,9 нс у локальной переменной — на треть дороже, две наносекунды
(3.13.7). Столько же, до сотых кратности, стоит и глобальная, вопреки
расхожему «глобальные медленные».
- что функцию можно определить внутри другой функции и вернуть её наружу как обычное значение;
- что у функции есть свои переменные — параметры и локальные, — которые живут, пока функция выполняется;
- что вызвать полученную функцию можно когда угодно позже, а не сразу.
- что такое ячейка, свободная переменная и позднее связывание — эти слова появятся по ходу и будут объяснены;
nonlocal,global,UnboundLocalError,co_freevars,LOAD_DEREF,MAKE_CELL.
База: внутренняя функция помнит переменные внешней
Начать стоит с самой обычной картины, в которой ещё нет ни одного специального слова: одна функция создаёт другую и возвращает её наружу.
def make_adder(step): # step — параметр внешней функции
def add(value):
return value + step # ...которым пользуется внутренняя
return add
add1 = make_adder(1) # make_adder отработала и завершилась
add1(10) # 11 — а step по-прежнему доступенПосмотрите на последние две строки. make_adder уже закончилась: её вызов
завершён, возвращать больше нечего. Обычно после этого локальные переменные и
параметры функции исчезают вместе с вызовом — они и нужны-то только на время
его. А step работает.
Вот главный вопрос урока: почему параметр внешней функции всё ещё существует после того, как внешняя функция завершилась?
Ответ, которого достаточно для базового уровня: потому что внутренняя функция
сохраняет доступ к переменной внешней. Она создавалась внутри make_adder,
пользовалась её step — и, уходя наружу, забрала этот доступ с собой. Пока жива
add1, жив и её step. Такая функция — та, что унесла с собой доступ к
переменным места, где была создана, — и называется замыканием.
Одну вещь в этом ответе стоит прочитать буквально: сохраняется доступ к переменной, а не копия её значения. Разница незаметна, пока переменная не меняется, и становится главной, как только меняется.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, как именно устроен этот доступ, что будет, если внутренних функций несколько, и в каких двух местах правило поиска имени работает не так, как его обычно пересказывают.
Механизм 1: ячейка вместо копии
Внутренняя функция, читающая имя из внешней, не получает его значение. Она получает ячейку — отдельный объект, через который к переменной обращаются обе стороны.
def outer():
x = 1
def inner():
return x
return innerРешение принимает компилятор, и видно оно в двух полях объекта кода:
outer.__code__.co_cellvars # ('x',) — здесь ячейка заводится
outer().__code__.co_freevars # ('x',) — здесь она читаетсяПоля заполнены ещё до первого вызова: код inner лежит внутри объекта кода
outer с момента компиляции. Во второй строке outer() вызывается только ради
краткости — чтобы добраться до этого кода через готовую функцию.
co_freevars — это и есть свободные переменные из определения справочника:
If a variable is used in a code block but not defined there, it is a free
variable
(Если переменная используется в блоке кода, но не определена в нём, она является свободной переменной). У функции без таких имён замыкания нет вовсе, и __closure__ равен
None — не пустому кортежу, а именно None.
Само значение лежит внутри ячейки и достаётся оттуда:
inner = outer()
inner.__closure__[0].cell_contents # 1Механизм 2: значение читается при вызове, а не при создании
Раз держится ячейка, а не копия, изменение переменной после создания функции доходит до неё:
def make():
v = "первое"
def show():
return v
v = "второе" # меняем после того, как show уже создана
return show
make()() # 'второе'Это не особенность вложенных функций. Справочник формулирует правило для всех
свободных имён — Name resolution of free variables occurs at runtime, not at
compile time
(Разрешение имён свободных переменных происходит во время выполнения, а не во время компиляции) — и приводит пример, который работает так же на уровне модуля:
i = 10
def f():
print(i)
i = 42
f() # 42Отсюда же растёт знаменитая ошибка с циклом: функции, созданные в цикле по трём
значениям, читают одну ячейку и все возвращают то, что осталось в ней после
последней итерации. Она подробно разобрана в уроке
про lambda — вместе с тем, что lambda тут ни
при чём: обычный def в цикле ломается ровно так же.
Механизм 3: одна ячейка на всех
Если из одной функции выйдут две внутренние, читающие одно имя, ячейка у них будет общая — не по копии на каждую:
def two():
x = 1
def a(): return x
def b(): return x
return a, b
a, b = two()
a.__closure__[0] is b.__closure__[0] # TrueЭто тот же объект, а не два равных: проверка идёт по is, то есть по
тождеству. Отсюда и берётся вся семья ошибок «почему они все возвращают одно и
то же»: менять нечего — ячейка одна, и в ней одно значение.
Механизм 4: ячейка держит объект живым
Ячейка — это ссылка, и всё, что про ссылки известно, к ней относится. Пока жива
функция, жива ячейка; пока жива ячейка, жив объект в ней. Отсюда класс утечек,
который в профайлере выглядит загадочно, а в коде — безобидно
(bench/closure-lifetime/retention.py, вывод одинаков на 3.12, 3.13 и 3.14).
Сначала — граница, которую полезно знать, чтобы не пугаться зря. Замыкание
держит не весь кадр, а только имена из co_freevars (здесь и ниже вывод
скрипта сокращён до относящихся к делу строк):
2) после выхода из make_callback:
упомянутый в теле data : жив
неупомянутый unused : мёртв
__closure__ замыкания : 1 ячейка(и)
co_freevars : ('data',)
Компилятор кладёт в ячейку ровно те имена, что действительно читаются в теле. Локальная переменная, которую вложенная функция не упоминает, умирает вместе с вызовом, как и без всякого замыкания. Документация описывает состав напрямую:
A tuple containing the names of free (closure) variables that a nested
scope references in an outer scope. […] Note: references to global and builtin
names are not included.
Кортеж, содержащий имена свободных переменных (переменных замыкания), на которые вложенная область видимости ссылается во внешней области. Замечание: ссылки на глобальные и встроенные имена сюда не входят.
Практический случай — обработчик, зарегистрированный где-то в реестре:
5) соединение отпущено вызывающим, обработчик в реестре: жив
что держит: ('self',)
после очистки реестра: мёртв
del conn тут не делает ничего: объект держит self в co_freevars
обработчика, а обработчик держит реестр. Это самая частая форма — метод, взятый
как колбэк, тянет за собой весь объект.
Второй случай тише: цикл через ячейку. Узел держит обработчик, обработчик через ячейку держит узел.
6) цикл «узел -> обработчик -> ячейка -> узел», сборщик выключен: жив
после gc.collect(): мёртв | собрано объектов: True
Счётчик ссылок такой цикл не разрывает — нужен именно сборщик циклов. Пока он не пришёл, память занята, и в момент выключенного или отложенного сбора это видно как рост.
Отпустить объект, не удаляя функцию, можно: ячейку разрешено переписать изнутри
через nonlocal.
7) до release: жив
после release(): мёртв | cb2() = отпущено
Ячейка при этом осталась — в ней теперь None. Это штатный способ сделать
«закрываемый» колбэк, и он же объясняет, почему weakref в таких местах решает
задачу целиком: слабой ссылки на объект недостаточно, чтобы объект остался жив.
Механизм 5: что где лежит — ячейка, снимок и partial
Ловушку lambda: i в цикле выше объясняли ячейкой. Способов на самом деле три, и
различить их полезно не ради выбора — результат в цикле у двух из них
одинаковый, — а ради того, чтобы понимать, что вы читаете, когда смотрите на
чужую функцию (bench/introspection/closure_vars.py, вывод одинаков на
3.12, 3.13 и 3.14):
8) три способа в цикле:
замыкание по i : [2, 2, 2]
аргумент по умолч. : [0, 1, 2]
functools.partial : [0, 1, 2]
6) хранилища:
closure: __closure__ = (<cell ...>,) -> cell_contents = ['второе']
default: __defaults__ = ('первое',) | __closure__ = None
partial: .args = ('первое',) | .keywords = {}
Три разных места. И различие не косметическое: только первое живое.
5) до переприсваивания: первое | первое | первое
после rebind('второе'): второе | первое | первое
замыкание видит новое значение, снимки — старое
Разница между аргументом по умолчанию и partial при этом мельче, чем принято
думать: оба берут снимок в момент создания, оба не видят переприсваивания.
Отличаются они местом хранения и тем, что __defaults__ можно переписать
снаружи, а partial неизменяем — нужен новый объект.
Инструмент, который показывает первое хранилище и не показывает остальные два, —
inspect.getclosurevars:
Get the mapping of external name references in a Python function or method
func to their current values. A named tuple ClosureVars(nonlocals, globals, builtins, unbound) is returned. nonlocals maps referenced names to lexical
closure variables, globals to the function's module globals and builtins to
the builtins visible from the function body. unbound is the set of names
referenced in the function that could not be resolved at all given the current
module globals and builtins.
Получить отображение внешних имён, на которые ссылается питоновская функция или метод func, в их текущие значения. Возвращается именованный кортеж ClosureVars(nonlocals, globals, builtins, unbound). nonlocals сопоставляет упомянутые имена переменным лексического замыкания, globals — глобальным именам модуля функции, builtins — встроенным именам, видимым из тела функции. unbound — множество имён, упомянутых в функции, которые вообще не удалось разрешить при текущих глобальных и встроенных именах модуля.
Он раскладывает внешние имена функции по четырём корзинам сразу:
1) getclosurevars(by_closure):
nonlocals: {'n': 2}
globals : {'GLOBAL_RATE': 10}
builtins : {'len': <built-in function len>}
unbound : set()
Корзина unbound — отдельно полезная: в неё попадают имена, которые в текущем
модуле не разрешаются вовсе. Опечатка в имени глобальной переменной видна тут
до вызова, а не в проде.
Но границу инструмента надо знать. Снимка в __defaults__ для него не
существует, а partial он вообще отвергает:
2) getclosurevars(by_default) — n НЕ в nonlocals, он параметр:
nonlocals: {}
а значение снимка лежит здесь: (2,)
3) getclosurevars(partial) -> TypeError: functools.partial(...) is not a Python function
значение снимка у partial: (2,) | func: body
То есть «замыкание пустое» в его выводе значит «нет ячеек», а не «функция ничего
не запомнила». Где искать запомненное — в __defaults__ и в .args.
Механизм 6: цепочка областей и два места, где она не такая
Четыре кнопки над схемой — четыре расклада для одного и того же имени x: в
трёх оно находится в разных областях, в четвёртом поиск обрывается на первой же
ступени. Тело класса стоит в списке отдельной строкой и зачёркнуто.
Правило просмотра формулируют как «локальная → объемлющая → глобальная → встроенные». Оно верное, но в нём не сказано о двух вещах.
Тело класса пропускается
Блоков в языке три: a module, a function body, and a class definition
(модуль, тело функции и определение класса). Тело
класса — блок, но в цепочку поиска имён для методов оно не входит:
def make():
x = "из функции"
class C:
x = "из класса"
def get(self):
return x # ← какое x?
return C
make()().get() # 'из функции'Метод берёт x из объемлющей функции, хотя в классе есть свой x и он
ближе. Справочник говорит это прямо: The scope of names defined in a class
block is limited to the class block; it does not extend to the code blocks of
methods
(Область видимости имён, определённых в блоке класса, ограничена этим блоком; она не распространяется на блоки кода методов). В PEP 227, которым вложенные области и появились, оговорка ещё
резче: If a class definition occurs in a chain of nested scopes, the resolution
process skips class definitions
(Если определение класса встречается в цепочке вложенных областей, разрешение имён пропускает определения классов).
Если объемлющей функции нет, имя просто не находится:
class C:
y = "класс"
def get(self):
return y # NameError: name 'y' is not definedОбращаться к атрибуту класса нужно через self или имя класса — то есть
атрибутом, а не свободным именем.
Присваивание делает имя локальным для всего тела
Второе место — не про порядок областей, а про то, какой области имя вообще принадлежит:
x = "глобальная"
def trap():
print(x) # UnboundLocalError
x = "локальная"Ошибка возникает на строке выше присваивания. Причина в том, что компилятор
решает это один раз для всего тела, и решение видно в объекте кода: имя
попадает в co_varnames — список локальных, — а не в co_names, куда попадают
имена, которые ищут снаружи.
trap.__code__.co_varnames # ('x',) — имя локальное, всё телоСообщение при этом точное: cannot access local variable 'x' where it is not associated with a value. Не «имя не найдено», а «локальная переменная есть, но
значения в ней ещё нет».
Механизм 7: nonlocal и global — две разные операции
Чтение свободного имени работает само. Чтобы присваивать внешней переменной, нужно объявить, какой именно:
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
c = counter()
c(), c(), c() # (1, 2, 3)nonlocal направляет имя, по формулировке справочника, to previously bound
variables in the nearest enclosing function scope
(к ранее связанным переменным в ближайшей объемлющей области функции). Слово «function» здесь
существенно по той же причине, что и в разделе про классы: тело класса под это
определение не подходит.
Ещё одно отличие важнее, чем кажется: nonlocal требует существующей
переменной, и её отсутствие — ошибка не времени выполнения, а компиляции:
def outer():
def inner():
nonlocal zz # SyntaxError: no binding for nonlocal 'zz' found
zz = 1Справочник говорит это прямо: If a name is not bound in any nonlocal scope, or
if there is no nonlocal scope, a
(Если имя не связано ни в одной нелокальной области видимости или такой области нет вовсе, возбуждается SyntaxError is raisedSyntaxError).
global устроен наоборот: он не требует, чтобы имя уже было, и направляет его
в модуль. Справочник: all uses of the names specified in the statement refer to
the bindings of those names in the top-level namespace
(все употребления перечисленных в инструкции имён обращаются к связкам этих имён в пространстве имён верхнего уровня), причём поиск идёт
сначала по глобальному пространству, потом по builtins.
nonlocal | global | |
|---|---|---|
| куда направляет | ближайшая объемлющая функция | пространство имён модуля |
| требует существующей переменной | да, иначе SyntaxError | нет |
| работает на верхнем уровне | нет | да |
Глубже: что видно в байт-коде
Ячейка живёт не только в документации: у неё есть свои инструкции. Внешняя функция заводит ячейку и пишет в неё, внутренняя её забирает и читает (здесь и далее — 3.13.7):
внешняя: внутренняя:
MAKE_CELL x COPY_FREE_VARS
LOAD_CONST 1 RESUME
STORE_DEREF x LOAD_DEREF x
LOAD_FAST x RETURN_VALUE
BUILD_TUPLE
MAKE_FUNCTION
SET_FUNCTION_ATTRIBUTE closure
Это два независимых листинга: каждый читается сверху вниз, построчного соответствия между колонками нет.
Обратите внимание на STORE_DEREF вместо привычного STORE_FAST: даже внутри
своей функции переменная, у которой есть ячейка, пишется через ячейку. Иначе
внутренняя функция читала бы не то, что записала внешняя.
Набор опкодов вокруг создания замыкания менялся:
| 3.12.3 | 3.13.7 | 3.14.7 | |
|---|---|---|---|
| ячейка кладётся в замыкание | LOAD_CLOSURE | LOAD_FAST | LOAD_FAST_BORROW |
| замыкание привязывается к функции | MAKE_FUNCTION closure | SET_FUNCTION_ATTRIBUTE closure | SET_FUNCTION_ATTRIBUTE closure |
Само же чтение — LOAD_DEREF — не менялось ни разу. Это ровно тот случай, о
котором стоит помнить, глядя на любой разбор байт-кода: имена инструкций
привязаны к версии, а механизм за ними — нет.
Глубже: сколько это стоит
Обращение к ячейке дороже обращения к локальной переменной, но настолько мало, что переписывать ради этого код бессмысленно. Двести обращений к одному имени подряд, минимум из девяти повторов, 3.13.7:
| доступ | опкод | на одно обращение | к локальной |
|---|---|---|---|
| локальная переменная | LOAD_FAST | 6,9 нс | ×1,00 |
| ячейка замыкания | LOAD_DEREF | 9,1 нс | ×1,32 |
| глобальная | LOAD_GLOBAL | 9,1 нс | ×1,32 |
Две наносекунды на обращение — треть от стоимости локальной. Много это или мало, видно из того, что на одно обращение приходится меньше десяти наносекунд: чтобы треть от них дала заметный выигрыш, имя должно читаться в цикле, который сам по себе и есть узкое место.
Отдельного внимания стоит третья строка: она совпала со второй, и это не округление в пользу удобного вывода. Прогон повторяет весь замер пять раз и сам сопоставляет расстояние между строками с собственным шатанием:
РАЗМАХ ПО 5 ПОВТОРАМ ВСЕГО ЗАМЕРА, в кратностях к локальной
LOAD_FAST (локальная) x1.00 .. x1.01 размах 0.01
LOAD_DEREF (ячейка) x1.32 .. x1.33 размах 0.01
LOAD_GLOBAL (глобальная) x1.32 .. x1.33 размах 0.01
расстояние между ячейкой и глобальной 0.00
наибольший размах этих же двух строк 0.01
различима ли разница прибором нет
«Глобальные переменные медленные» — совет, который переживает свои основания: прибор не видит между глобальной и ячейкой никакой разницы вовсе. Причина в том, что интерпретатор специализирует обращение к глобальному имени, если словарь модуля не менялся; разбор этого механизма — в статье про адаптивный интерпретатор.
Здесь же проходит граница этого утверждения, и записать правило стоит от неё, а не от числа. «Глобальная стоит столько же» верно там, где специализация работает: CPython 3.13.7, словарь модуля не переписывается на ходу. Правило, которое выдерживает перенос, звучит иначе: на этих величинах вид доступа выбирают не по цене, а по смыслу — а если решение всё-таки упирается в наносекунды, их меряют на своём коде и своей сборке.
Время между версиями здесь не сравнивается, и вот почему. 3.12
собран GCC 13.3.0, а 3.13 и 3.14 — Clang 20.1.4, причём у 3.14 вдобавок включён
--with-tail-call-interp, которого у 3.13 нет. Различаются не только
компиляторы, и разница сборок перекрывает разницу версий. Сравнивать можно
только строки одной таблицы: они сняты одним запуском на 3.13.7.
История версий
Python 2.1 — PEP 227 вводит вложенные области: имя из объемлющей функции становится видно. Там же зафиксировано исключение для тел классов, действующее и сегодня.
Python 3 — появляется nonlocal, то есть возможность не только читать
внешнюю переменную, но и присваивать ей; спецификацией оператора справочник
называет PEP 3104.
Python 3.11 — в байт-коде появляется явный MAKE_CELL: заведение ячейки
становится отдельной инструкцией в начале кода функции. У неё, как и у
COPY_FREE_VARS, в документации dis стоит Added in version 3.11
(Добавлено в версии 3.11).
Python 3.13 — LOAD_CLOSURE уходит из листинга: замыкание привязывается к
функции отдельной инструкцией SET_FUNCTION_ATTRIBUTE, датированной в
документации как Added in version 3.13
(Добавлено в версии 3.13).
Python 3.14 — загрузка ячейки в кортеж замыкания идёт через
LOAD_FAST_BORROW, как и прочие загрузки локальных. Наблюдаемое поведение
прежнее.
Как отвечать на собеседовании
Короткий ответ: замыкание — это функция, которая сохранила доступ к переменным места, где была создана, и пользуется ими после того, как это место отработало. Сохранён именно доступ к переменной, а не копия её значения: значение читается в момент вызова. Поэтому изменение переменной после создания функции до неё доходит.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Объект, через который идёт этот доступ, называется ячейкой, и справочник
формулирует правило прямо: разрешение имён свободных переменных происходит во
время выполнения, а не во время компиляции. Ячейка при этом одна на всех: две
внутренние функции, читающие одно имя, ссылаются на один объект — проверяется
сравнением по is.
Что отличает хороший ответ: назвать то, о чём привычная цепочка «локальная →
объемлющая → глобальная → встроенные» умалчивает. Тело класса в цепочку не
входит: метод, читающий x, возьмёт его из объемлющей функции, даже если
рядом в классе есть свой. И присваивание где угодно в теле делает имя
локальным для всего тела — обращение к нему выше по коду даёт
UnboundLocalError, не добираясь до глобальной переменной с тем же именем.
Цена всего этого мала: 9,1 нс против 6,9 у локальной — две наносекунды, — и
ровно столько же стоит глобальная, вопреки расхожему «глобальные медленные».
Дальше спросят
Как тогда сделать, чтобы каждая функция из цикла помнила своё значение?
Задать его аргументом со значением по умолчанию или через functools.partial —
то есть положить снимок, а не ячейку. Замыкание держит переменную, и
значение читается в момент вызова: изменение переменной после создания функции
до неё доходит.
Замыкание держит ссылку. Чем это грозит по памяти?
Классом утечек, который в профайлере выглядит загадочно, а в коде безобидно:
пока жива функция — жива ячейка, пока жива ячейка — жив объект в ней. Границу
полезно знать, чтобы не пугаться зря: держится не весь кадр, а только имена из
co_freevars.
Частые заблуждения
«Замыкание сохраняет значение переменной на момент создания функции».
Оно сохраняет ячейку. Переменная, изменённая после создания функции, доходит до неё: v меняется на «второе» уже после def show, и show() возвращает «второе». Справочник формулирует это как правило: Name resolution of free variables occurs at runtime, not at compile time
(разрешение имён свободных переменных происходит во время выполнения, а не во время компиляции).
«Каждая внутренняя функция получает свою копию внешней переменной».
Ячейка одна на всех. Две функции, читающие одно имя, дают a.__closure__[0] is b.__closure__[0] → True: это один объект, а не два равных. Отсюда же и семья ошибок «почему все возвращают одно и то же».
«Метод видит атрибуты своего класса как обычные имена».
Тело класса в цепочку поиска не входит. Метод, читающий x, возьмёт его из объемлющей функции, даже если в классе есть свой x, а без объемлющей функции получит NameError. Справочник: The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods
(область видимости имён, определённых в блоке класса, ограничена этим блоком; она не распространяется на блоки кода методов). К атрибуту обращаются через self или имя класса.
«UnboundLocalError означает, что переменную забыли создать».
Он означает, что имя локальное — из-за присваивания где-то ниже в теле, — и значения в нём пока нет. Глобальная переменная с тем же именем при этом существует, но до неё не доходят. Решение принято компилятором для всего тела сразу, и его видно в co_varnames.
«Обращение к глобальным переменным заметно медленнее».
На 3.13.7 глобальная стоит 9,1 нс на обращение — ровно столько же, сколько ячейка замыкания: в пяти повторах замера расстояние между ними 0,00 при размахе каждой 0,01, то есть прибор их не различает вовсе. Дороже локальной обе, на две наносекунды — треть, но треть от девяти наносекунд.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
x = "module"
def outer():
x = "enclosing"
class C:
x = "class"
def read(self):
return x
return C().read()
def shadow():
try:
got = x
except UnboundLocalError:
return "UnboundLocalError"
x = "local"
return got
print(outer())
print(shadow())Практика · оцените
Проверка знаний
Функция make объявляет x = 'из функции', внутри неё класс C с атрибутом x = 'из класса', а метод get возвращает свободное x. Что вернёт make()().get()?
Чем измерено
Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Функция, созданная внутри другой, продолжает пользоваться её переменными и после того, как внешняя функция завершилась. Это и есть замыкание. Важно, чем именно она пользуется: не копией значения, снятой в момент создания, а самой переменной — поэтому значение читается тогда, когда внутреннюю функцию вызывают, а не тогда, когда её создавали.
- Отсюда главное следствие: захвачена переменная, а не значение, и расходится это с ожиданием в трёх местах. Изменение переменной после создания функции до неё доходит:
vменяется на «второе» уже послеdef show, иshow()возвращает «второе». Доступ к переменной при этом общий: две внутренние функции, читающие одно имя, ссылаются на один и тот же объект — проверено сравнением поis. А привычная цепочка «локальная → объемлющая → глобальная → встроенные» умалчивает о двух вещах: тело класса в неё не входит (метод, читающийx, возьмётxиз объемлющей функции, даже если рядом в классе есть свой) и присваивание где угодно в теле делает имя локальным для всего тела — обращение к нему выше по коду даётUnboundLocalError, не добираясь до глобальной переменной с тем же именем. - Дальше — то, что отличает знающего от читавшего. Объект, через который идёт общий доступ, называется ячейкой, а имя, которое функция использует, но не определяет, — свободной переменной; справочник говорит про них прямо: Name resolution of free variables occurs at runtime, not at compile time. Цена всего этого мала и при этом различима: обращение к ячейке стоит 9,1 нс против 6,9 нс у локальной переменной — на треть дороже, две наносекунды (3.13.7). Столько же, до сотых кратности, стоит и глобальная, вопреки расхожему «глобальные медленные».
На самом деле
- Оно сохраняет ячейку. Переменная, изменённая после создания функции, доходит до неё:
vменяется на «второе» уже послеdef show, иshow()возвращает «второе». Справочник формулирует это как правило: Name resolution of free variables occurs at runtime, not at compile time. - Ячейка одна на всех. Две функции, читающие одно имя, дают
a.__closure__[0] is b.__closure__[0]→True: это один объект, а не два равных. Отсюда же и семья ошибок «почему все возвращают одно и то же». - Тело класса в цепочку поиска не входит. Метод, читающий
x, возьмёт его из объемлющей функции, даже если в классе есть свойx, а без объемлющей функции получитNameError. Справочник: The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods. К атрибуту обращаются черезselfили имя класса. - Он означает, что имя локальное — из-за присваивания где-то ниже в теле, — и значения в нём пока нет. Глобальная переменная с тем же именем при этом существует, но до неё не доходят. Решение принято компилятором для всего тела сразу, и его видно в
co_varnames. - На 3.13.7 глобальная стоит 9,1 нс на обращение — ровно столько же, сколько ячейка замыкания: в пяти повторах замера расстояние между ними 0,00 при размахе каждой 0,01, то есть прибор их не различает вовсе. Дороже локальной обе, на две наносекунды — треть, но треть от девяти наносекунд.
Что разобрано
- База: внутренняя функция помнит переменные внешней
- Механизм 1: ячейка вместо копии
- Механизм 2: значение читается при вызове, а не при создании
- Механизм 3: одна ячейка на всех
- Механизм 4: ячейка держит объект живым
- Механизм 5: что где лежит — ячейка, снимок и `partial`
- Механизм 6: цепочка областей и два места, где она не такая
- Механизм 7: `nonlocal` и `global` — две разные операции
- Глубже: что видно в байт-коде
- Глубже: сколько это стоит
- История версий
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
- Чем измерено
Источники и что читать дальше
5 ИСТОЧНИКОВ
- Справочник языка — модель выполнения, области и связывание имёнОфициальная документация. Первоисточник всех правил урока. Блок: «A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition» (Блок — это фрагмент текста программы на Python, выполняемый как единое целое. Блоками являются: модуль, тело функции и определение класса). Свободная переменная: «If a variable is used in a code block but not defined there, it is a free variable» (Если переменная используется в блоке кода, но не определена в нём, она является свободной переменной). И главное для урока: «Name resolution of free variables occurs at runtime, not at compile time» (Разрешение имён свободных переменных происходит во время выполнения, а не во время компиляции) — там же приведён пример с i = 10 / i = 42, воспроизведённый в тексте.https://docs.python.org/3.14/reference/executionmodel.html
- Справочник языка — тело класса не продолжает цепочкуОфициальная документация. Дословно: «The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods» (Область видимости имён, определённых в блоке класса, ограничена этим блоком; она не распространяется на блоки кода методов). Отсюда следствие для методов: атрибут класса доступен через self или имя класса, но не как свободное имя. Это одно из двух мест, где привычная цепочка областей даёт осечку (второе — не про порядок областей, а про принадлежность имени). Проверено запуском на 3.12, 3.13 и 3.14.7.https://docs.python.org/3.14/reference/executionmodel.html
- Справочник языка — оператор nonlocalОфициальная документация. Полная спецификация nonlocal, откуда взяты оба утверждения урока. Про ближайшую область: «If a name is bound in more than one nonlocal scope, the nearest binding is used» (Если имя связано более чем в одной нелокальной области видимости, используется ближайшая связка). Про отказ ещё до запуска — дословно: «If a name is not bound in any nonlocal scope, or if there is no nonlocal scope, a SyntaxError is raised» (Если имя не связано ни в одной нелокальной области видимости или такой области нет вовсе, возбуждается SyntaxError). Там же справочник называет спецификацией оператора PEP 3104. Проверено компиляцией на 3.12, 3.13 и 3.14.7.https://docs.python.org/3.14/reference/simple_stmts.html#the-nonlocal-statement
- PEP 227 — Statically Nested ScopesPEP. Джереми Хилтон; Final, Python 2.1. PEP, которым вложенные области вообще появились: «The proposal changes the rules so that names bound in B are visible in A» (Предложение меняет правила так, что имена, связанные в B, становятся видны в A). Он же прямо оговаривает исключение, из-за которого написан раздел про классы: «Names in class scope are not accessible. Names are resolved in the innermost enclosing function scope. If a class definition occurs in a chain of nested scopes, the resolution process skips class definitions» (Имена в области видимости класса недоступны. Имена разрешаются в ближайшей объемлющей области функции. Если определение класса встречается в цепочке вложенных областей, разрешение пропускает определения классов).https://peps.python.org/pep-0227/
- dis — MAKE_CELL, STORE_DEREF, LOAD_DEREF, COPY_FREE_VARSОфициальная документация. Откуда взяты опкоды в разделе про байт-код и их смена по версиям: на 3.12 ячейка кладётся в замыкание через LOAD_CLOSURE и MAKE_FUNCTION с флагом closure, на 3.13 и 3.14 — через обычную загрузку и отдельный SET_FUNCTION_ATTRIBUTE. Оттуда же датировка инструкций: у MAKE_CELL и COPY_FREE_VARS стоит «Added in version 3.11» (Добавлено в версии 3.11), у SET_FUNCTION_ATTRIBUTE — «Added in version 3.13» (Добавлено в версии 3.13). Разобрано запуском dis на всех трёх версиях.https://docs.python.org/3.14/library/dis.html