Замыкания и лямбды в циклах Python
@python_quizРазберем этот квиз
Условие — код для анализа

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

Почему все три значения одинаковые? Дальше подробное объяснение.
Почему так происходит (производная от замыкания)
- Лямбда здесь создаётся как замыкание, которое обращается к переменной
i. - В Python замыкания захватывают переменные по ссылке (late binding), то есть замыкание хранит ссылку на саму переменную
i, а не на её значение в момент создания функции. - После завершения цикла переменная
iимеет последнее значение изrange(3), то есть2. - При последующем вызове каждой лямбды они все читают текущее значение
i— и получается[2, 2, 2].
Проще: все функции смотрят на одну и ту же переменную i, и к моменту вызова её значение равно 2.
Анализ вариантов ответа (кратко)
- Вариант, где все элементы равны 2 — соответствует поведению из-за позднего связывания замыканий.
- Варианты с
[0, 1, 2]или[0, 0, 0]предполагают, что лямбды захватили значения на момент создания — это не так по умолчанию. - Вариант с
NameErrorневерен: переменнаяiсуществует и доступна в момент вызова лямбд (она живёт в окружающей области видимости).
Как получить ожидаемое «значение при создании» — способы фиксации значения
Если нужно, чтобы каждая лямбда запоминала текущее значение i при создании, есть несколько простых приёмов.
1) Использовать аргумент по умолчанию в лямбде (самый распространённый трюк):

Аргументы по умолчанию вычисляются в момент создания функции и хранят конкретные значения.
2) Создать фабрику функций:

Здесь каждая вызванная make создаёт новую локальную переменную i, которая фиксируется в замыкании.
3) Использовать functools.partial (иногда полезно, если нужна более сложная конфигурация):

partial фиксирует значение аргумента при создании объекта-обёртки.
Небольшие замечания
- Это поведение однотипно для Python 3 (и для Python 2 — в том числе; отличие не в механизме замыкания, а в том, что в Python 2 range возвращал список, но сам цикл всё равно задавал
i). - Частая ошибка у новичков — ожидать «копирование» значения при создании функции в цикле. Полезно помнить правило: замыкание хранит имя переменной, а не её снимок.
Вывод
Когда лямбда или любая функция внутри цикла использует переменную из внешнего окружения, она обращается к этой переменной по ссылке. Чтобы «зафиксировать» значение переменной в момент создания функции, нужно явно сохранить его — например, через аргумент по умолчанию или через фабрику функций. Это простой и часто используемый паттерн при работе с замыканиями в Python.