Deep Engineering
Средний·Опубликовано·3.12 · 3.13 · 3.14·35 МИН

Замыкания: захвачена переменная, а не значение — и цепочка областей проходит мимо тела класса

Замыкание держит ячейку, а не копию: значение читается в момент вызова, и одна ячейка достаётся всем внутренним функциям сразу. Привычная цепочка «локальная → объемлющая → глобальная → встроенные» умалчивает о двух вещах, на которых и ошибаются: тело класса в неё не входит, а присваивание где угодно в теле делает имя локальным для всего тела.

Полное техническое изложение

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.

База: внутренняя функция помнит переменные внешней

Начать стоит с самой обычной картины, в которой ещё нет ни одного специального слова: одна функция создаёт другую и возвращает её наружу.

PYTHON
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: ячейка вместо копии

контракт языкаГарантия языка: замыкание захватывает ячейку, а не значение. Из этого следует всё остальное в уроке.

Внутренняя функция, читающая имя из внешней, не получает его значение. Она получает ячейку — отдельный объект, через который к переменной обращаются обе стороны.

PYTHON
def outer():
    x = 1
    def inner():
        return x
    return inner

Решение принимает компилятор, и видно оно в двух полях объекта кода:

PYTHON
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.

Само значение лежит внутри ячейки и достаётся оттуда:

PYTHON
inner = outer()
inner.__closure__[0].cell_contents   # 1

Механизм 2: значение читается при вызове, а не при создании

Раз держится ячейка, а не копия, изменение переменной после создания функции доходит до неё:

PYTHON
def make():
    v = "первое"
    def show():
        return v
    v = "второе"          # меняем после того, как show уже создана
    return show
 
make()()                  # 'второе'

Это не особенность вложенных функций. Справочник формулирует правило для всех свободных имён — Name resolution of free variables occurs at runtime, not at compile time (Разрешение имён свободных переменных происходит во время выполнения, а не во время компиляции) — и приводит пример, который работает так же на уровне модуля:

PYTHON
i = 10
def f():
    print(i)
i = 42
f()                       # 42

Отсюда же растёт знаменитая ошибка с циклом: функции, созданные в цикле по трём значениям, читают одну ячейку и все возвращают то, что осталось в ней после последней итерации. Она подробно разобрана в уроке про lambda — вместе с тем, что lambda тут ни при чём: обычный def в цикле ломается ровно так же.

Механизм 3: одна ячейка на всех

Если из одной функции выйдут две внутренние, читающие одно имя, ячейка у них будет общая — не по копии на каждую:

PYTHON
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.

перевод

Кортеж, содержащий имена свободных переменных (переменных замыкания), на которые вложенная область видимости ссылается во внешней области. Замечание: ссылки на глобальные и встроенные имена сюда не входят.

Модель данных — codeobject.co_freevars

Практический случай — обработчик, зарегистрированный где-то в реестре:

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 — множество имён, упомянутых в функции, которые вообще не удалось разрешить при текущих глобальных и встроенных именах модуля.

inspect — getclosurevars

Он раскладывает внешние имена функции по четырём корзинам сразу:

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 (модуль, тело функции и определение класса). Тело класса — блок, но в цепочку поиска имён для методов оно не входит:

PYTHON
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 (Если определение класса встречается в цепочке вложенных областей, разрешение имён пропускает определения классов).

Если объемлющей функции нет, имя просто не находится:

PYTHON
class C:
    y = "класс"
    def get(self):
        return y            # NameError: name 'y' is not defined

Обращаться к атрибуту класса нужно через self или имя класса — то есть атрибутом, а не свободным именем.

Присваивание делает имя локальным для всего тела

Второе место — не про порядок областей, а про то, какой области имя вообще принадлежит:

PYTHON
x = "глобальная"
 
def trap():
    print(x)                # UnboundLocalError
    x = "локальная"

Ошибка возникает на строке выше присваивания. Причина в том, что компилятор решает это один раз для всего тела, и решение видно в объекте кода: имя попадает в co_varnames — список локальных, — а не в co_names, куда попадают имена, которые ищут снаружи.

PYTHON
trap.__code__.co_varnames   # ('x',)  — имя локальное, всё тело

Сообщение при этом точное: cannot access local variable 'x' where it is not associated with a value. Не «имя не найдено», а «локальная переменная есть, но значения в ней ещё нет».

Механизм 7: nonlocal и global — две разные операции

Чтение свободного имени работает само. Чтобы присваивать внешней переменной, нужно объявить, какой именно:

PYTHON
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 требует существующей переменной, и её отсутствие — ошибка не времени выполнения, а компиляции:

PYTHON
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 raised (Если имя не связано ни в одной нелокальной области видимости или такой области нет вовсе, возбуждается SyntaxError).

global устроен наоборот: он не требует, чтобы имя уже было, и направляет его в модуль. Справочник: all uses of the names specified in the statement refer to the bindings of those names in the top-level namespace (все употребления перечисленных в инструкции имён обращаются к связкам этих имён в пространстве имён верхнего уровня), причём поиск идёт сначала по глобальному пространству, потом по builtins.

nonlocalglobal
куда направляетближайшая объемлющая функцияпространство имён модуля
требует существующей переменнойда, иначе SyntaxErrorнет
работает на верхнем уровненетда

Глубже: что видно в байт-коде

деталь реализации · CPython 3.13Байт-код и имена инструкций — устройство конкретной версии: они уже менялись и изменятся снова.

Ячейка живёт не только в документации: у неё есть свои инструкции. Внешняя функция заводит ячейку и пишет в неё, внутренняя её забирает и читает (здесь и далее — 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.33.13.73.14.7
ячейка кладётся в замыканиеLOAD_CLOSURELOAD_FASTLOAD_FAST_BORROW
замыкание привязывается к функцииMAKE_FUNCTION closureSET_FUNCTION_ATTRIBUTE closureSET_FUNCTION_ATTRIBUTE closure

Само же чтение — LOAD_DEREF — не менялось ни разу. Это ровно тот случай, о котором стоит помнить, глядя на любой разбор байт-кода: имена инструкций привязаны к версии, а механизм за ними — нет.

Глубже: сколько это стоит

наблюдение замераbench/closures/scopes.py, CPython 3.13.7. Числа сняты на одной машине; содержательна разница между строками, а не абсолют.

Обращение к ячейке дороже обращения к локальной переменной, но настолько мало, что переписывать ради этого код бессмысленно. Двести обращений к одному имени подряд, минимум из девяти повторов, 3.13.7:

доступопкодна одно обращениек локальной
локальная переменнаяLOAD_FAST6,9 нс×1,00
ячейка замыканияLOAD_DEREF9,1 нс×1,32
глобальнаяLOAD_GLOBAL9,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.13LOAD_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 объявлено трижды: в модуле, в объемлющей функции и в теле класса. Метод читает x. И вторая функция читает x до того, как ему присвоит. Что напечатает этот код?
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())

Практика · оцените

Чтение обычной локальной переменной и чтение ячейки замыкания. Во сколько раз дороже ячейка?
раза

Проверка знаний

Вопрос 1 из 4

Функция make объявляет x = 'из функции', внутри неё класс C с атрибутом x = 'из класса', а метод get возвращает свободное x. Что вернёт make()().get()?

Чем измерено

Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

5 ИСТОЧНИКОВ

  1. Справочник языка — модель выполнения, области и связывание имёнОфициальная документация. Первоисточник всех правил урока. Блок: «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
  2. Справочник языка — тело класса не продолжает цепочкуОфициальная документация. Дословно: «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
  3. Справочник языка — оператор 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
  4. 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/
  5. 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