UpPetto

This is a real course generated by UpPetto — unedited.

AI-generated. Two models wrote it, a third checked every risky claim against sources, a fourth re-checked.

Create my own course

Замыкания, LEGB и nonlocal: функции как объекты первого класса

core написать замыкание, которое корректно захватывает внешнюю переменную, и объяснить типичную ошибку захвата переменной цикла

В C функция — это, по сути, указатель на код, и у неё нет собственного состояния. Если callback'у нужны данные, вы вручную собираете структуру контекста и передаёте указатель на неё в каждый вызов:

struct counter_context { int count; };

void callback(void* context) {
    struct counter_context* ctx = (struct counter_context*)context;
    ctx->count++;
    printf("Count is now %d\n", ctx->count);
}

Функция и её данные — две отдельные сущности, связанные вручную.

В Python это встроено в язык: функция, определённая внутри другой функции, автоматически «захватывает» переменные из окружающей области видимости. Такая пара «функция + захваченные переменные» называется замыканием (closure). Это прямая замена паттерна «контекст + callback», только структуру контекста и связывание генерирует сам интерпретатор.

Правило LEGB и nonlocal

Чтобы понимать, что вообще захватывается, нужно правило поиска имён LEGB: Local → Enclosing → Global → Built-in. Интерпретатор ищет имя сначала в локальной области текущей функции, затем в областях всех объемлющих функций (от ближайшей наружу), затем в глобальной области модуля и наконец среди встроенных имён (len, print, range).

def make_counter():
    count = 0                  # Enclosing для inner
    def inner():
        nonlocal count         # без этого — UnboundLocalError на count += 1
        count += 1
        return count
    return inner

c = make_counter()
print(c(), c(), c())  # 1 2 3

Ключевая ловушка: присваивание внутри вложенной функции по умолчанию создаёт новую локальную переменную, а не изменяет внешнюю. count += 1 без nonlocal — это попытка прочитать count как локальную переменную до того, как ей что-то присвоено, отсюда UnboundLocalError. nonlocal — явная декларация «это имя из объемлющей функции, не создавай новую локальную копию». Для чтения nonlocal не нужен.

flowchart TD
    L["Local — переменные внутри самой функции"] --> E["Enclosing — переменные объемлющих функций"]
    E --> G["Global — переменные модуля"]
    G --> B["Built-in — встроенные имена (len, range...)"]
Порядок поиска имени по правилу LEGB — от локальной области наружу.
graph TD
    subgraph MAKER["кадр make_counter()"]
        A["count = 0"]
        subgraph INNER["кадр inner()"]
            B["nonlocal count"]
            C["count += 1"]
        end
    end
    C --> B
    B -->|"то же самое имя, та же ячейка"| A
nonlocal указывает, что count внутри inner — та же переменная, что и count в make_counter.

Ловушка: захват переменной цикла

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2], не [0, 1, 2]!

Причина: замыкание захватывает имя i, а не его значение в момент создания лямбды. Значение ищется в момент вызова (позднее связывание, late binding), а к этому времени цикл завершён и i равно 2. Это аналог ситуации в C, когда несколько указателей ссылаются на один и тот же адрес переменной цикла — только в Python это происходит неявно, без единого &i в коде.

Все лямбды в цикле ссылаются на одну и ту же переменную i, а не на её значение в момент создания.
Все лямбды в цикле ссылаются на одну и ту же переменную i, а не на её значение в момент создания.

Исправление — зафиксировать значение как default-аргумент (он вычисляется при создании функции, то есть отдельно на каждой итерации):

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

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

Разобранный пример

Набор валидаторов «больше порога». Наивная версия страдает от позднего связывания:

validators = []
for min_val in [10, 20, 30]:
    validators.append(lambda value: value > min_val)

val_10, val_20, val_30 = validators
print(val_10(15))   # False — ожидали True: все смотрят на min_val == 30

Фабричная функция решает проблему структурно:

def make_validator(min_val):
    def is_greater_than(value):
        return value > min_val      # min_val — Enclosing-переменная этого вызова
    return is_greater_than

val_10, val_20, val_30 = [make_validator(m) for m in (10, 20, 30)]
print(val_10(15), val_20(25), val_30(35))  # True True True

Каждый вызов make_validator создаёт новую область видимости со своим min_val, и is_greater_than захватывает именно её. В C это потребовало бы структуры struct validator { int threshold; } и функции, принимающей её первым аргументом — здесь структуру и связывание сгенерировал интерпретатор.

Попробуй сейчас

  1. Напишите make_accumulator(), возвращающую замыкание add(x), которое накапливает сумму между вызовами (без глобальных переменных, только nonlocal):
acc = make_accumulator()
print(acc(10))  # 10
print(acc(5))   # 15
print(acc(1))   # 16
  1. Воспроизведите баг с захватом переменной цикла на списке из трёх замыканий и исправьте его двумя способами: через default-аргумент и через фабричную функцию.

Получилось, если…

Вы разобрались, если можете объяснить, почему for i in range(3): funcs.append(lambda: i) даёт три одинаковых результата, назвать это следствием захвата имени (LEGB + позднее связывание), предложить фикс через lambda i=i: i или фабрику без подсказки, и сказать, зачем в make_accumulator нужен nonlocal, а в make_validator — нет.

Вывод

Замыкание захватывает имя из объемлющей области, а не копию значения — это ближе к разделяемому указателю, чем к передаче по значению. nonlocal нужен только для присваивания внешней переменной внутри вложенной функции; для чтения он не требуется. Переменные цикла в замыканиях фиксируйте default-аргументом или фабричной функцией.

AI-generated · source-grounded review

🛡 Fact-checked: 0 risky claims verified · 1 removed · confidence: high · figures: 3
[unconfirmed by second model] Замыкание захватывает имя (ячейку) из объемлющей области, а не копию значения; значение ищется в момент вызова (late binding)
Соответствует модели 'имена — привязки к объектам' и ссылочной семантике из базы знаний
Second model: База знаний не содержит раздела о замыканиях, захвате имён или позднем связывании.
[unconfirmed by second model] Порядок поиска имён — Local → Enclosing → Global → Built-in
Стандартное правило LEGB; согласовано между обеими версиями
Second model: База знаний не описывает правило поиска имён LEGB.
[unconfirmed by second model] count += 1 в вложенной функции без nonlocal даёт UnboundLocalError
Присваивание делает имя локальным; стандартное поведение интерпретатора
Second model: База знаний не упоминает nonlocal или UnboundLocalError в контексте вложенных функций.
[removed] Интроспекция замыкания через func.__closure__[0].cell_contents
Деталь реализации, отсутствующая в базе знаний и не нужная для выводов урока
[removed] Убран пример интроспекции через `__closure__[0].cell_contents` (деталь реализации, отсутствующая в базе знаний, и не нужна для вывода урока)

A second model re-checked this lesson's claims and corroborated 0 of 4 , and could not confirm 4 either way. This is incomplete corroboration, not a disagreement.

Key concepts: closure LEGB nonlocal
Tell me more 🔒 Didn't understand — explain simply 🔒 Show examples 🔒 Sources 🔒

On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course

Check yourself

1. Что выведет этот код? ```python funcs = [] for i in range(3): funcs.append(lambda: i) print([f() for f in funcs]) ```
2. Почему `count += 1` внутри вложенной функции без `nonlocal` вызывает UnboundLocalError, если count определена во внешней функции?
3. В каком порядке Python ищет имя по правилу LEGB?
On your own course, these are marked as you answer