Замыкания и лямбды в Python: почему вывод [2, 2, 2]
@python_quizРазберем этот квиз
Код квиза:

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

Вы увидите, что все лямбды ссылаются на одну и ту же ячейку памяти с содержимым 2.
Как получить [0, 1, 2] — способы привязать текущее значение
1) Использовать значение по умолчанию аргумента функции (самый распространённый и краткий прием):

Аргумент i=i создаёт новую локальную переменную со значением текущего i в момент создания лямбды.
2) Использовать фабрику функций (явно создать замыкание с собственной переменной):

3) Использовать functools.partial:

Все эти подходы создают для каждой функции своё собственное значение, вместо ссылки на общую изменяемую переменную.
Анализ вариантов ответа (коротко)
- Вариант "Напечатает [0, 1, 2]" — возможен, но только если при создании лямбд специально захватить значение i (например, через аргумент по умолчанию). В приведённом коде это не сделано.
- Вариант "Напечатает [2, 2, 2]" — соответствует поведению исходного кода: лямбды используют финальное значение i (late binding).
- Вариант "Напечатает [0, 0, 0]" — неверен: нет механизма, который зафиксировал бы первоначальное значение i для всех лямбд одинаково.
- Вариант "Бросит
NameError" — неверен: i доступна в месте вызова через замыкание, поэтому ошибки доступа не происходит.
Вывод
Поведение лямбд в данном примере объясняется принципом late binding: каждая лямбда ссылается на одну и ту же переменную i, и при вызове все возвращают её текущее (финальное) значение. Чтобы создать набор функций, каждая из которых "помнит" своё значение цикла, нужно явно привязать значение при создании (например, через default-аргумент lambda i=i: i или фабрику функций).