Замыкания и "late binding" в Python: почему вывод [4, 4, 4]
@python_quizРазберем этот квиз
Исходный фрагмент:

Коротко — при выполнении получится [4, 4, 4]. Ниже объясню почему, разберу распространённые заблуждения и покажу несколько корректных способов получить ожидаемое поведение.
Что происходит (пошагово)
- Вы создаёте список лямбд внутри генератора списка: для каждого значения
jформируетсяlambda i: i*j. - Лямбда захватывает переменную
jне как её текущее значение, а как ссылку на эту переменную (поведение называется late binding). - После завершения цикла в области видимости comprehension последним присвоенным значением
jбудет2(приrange(3)). - Все лямбды ссылаются на ту же самую переменную
j, поэтому при вызовеf(2)для каждой лямбдыj == 2и вычисление даёт2 * 2 = 4. - Итоговый список результатов:
[4, 4, 4].
Важно: в Python 3 переменная цикла генератора списка не «протекает» в внешнюю область видимости, но это не мешает замыканиям внутри comprehension ссылаться на эту переменную и испытывать эффект поздней привязки.
Демонстрация

Как получить ожидаемый (различный) результат
Если цель — чтобы каждая лямбда запомнила текущее значение j на момент её создания (т.е. получить [0*2, 1*2, 2*2] => [0, 2, 4]), есть несколько простых приёмов.
1) Захват значения через аргумент по умолчанию

Аргументы по умолчанию вычисляются в момент создания лямбды, поэтому j фиксируется.
2) Фабрика (функция-замыкание)

Каждому вызову make_mul создаётся своя локальная переменная j, и лямбда замыкает именно её.
3) functools.partial или оператор multiplication (альтернативный вариант)

partial связывает аргумент в момент создания, фиксируя значение.
Частые ошибки и пояснения
- Ошибка: думать, что лямбда копирует значение переменной автоматически. Она копирует ссылку на переменную.
- Ошибка: путать поведение в Python 2 и Python 3 относительно области видимости генераторов. В Python 3 переменная цикла comprehension не остается в внешнем scope, но замыкания внутри comprehension всё равно захватывают эту переменную в её (локальной для comprehension) области, и late binding остаётся актуальным.
- Надёжный и понятный стиль — явно фиксировать значение через аргумент по умолчанию или использовать фабрику функций.
Выводы
- Лямбды и функции в Python замыкают переменные по ссылке; итоговое значение определяется во время вызова, а не во время создания.
- Чтобы зафиксировать текущее значение переменной при создании замыкания, используйте аргументы по умолчанию или фабрики функций.
- Понимание этой особенности убережёт от неожиданных результатов и ошибок в коде, особенно при создании коллекций функций в циклах.