Почему выражение с lambda и comprehensions выводит "0 2"
@python_quizРазберем этот квиз
Кратко: код

выводит

Ниже подробно разберём, почему так, и как это поведение воспроизвести и избежать.
Что делает выражение — разбиваем по частям
Есть две части:
- next((lambda: i for i in range(3)))()
- Создаётся генераторное выражение, которое в цикле for i in range(3) порождает объекты lambda: i.
- Вызов next(...) выполняет генератор до первого yield и возвращает первую сгенерированную лямбду. При этом генератор приостановлен на первом шаге и переменная i в его фрейме равна 0. Вызов полученной лямбды сразу же читает текущее значение i — это 0.
- [lambda: i for i in range(3)][0]()
- Список создаётся полностью; цикл for i in range(3) выполняется до конца, создавая три лямбды, каждая из которых закрывает переменную i из области comprehension. После завершения цикла переменная i имеет значение 2 (последнее значение итерации). Все лямбды ссылаются на ту же переменную i, поэтому при вызове любой из них читается финальное значение 2.
Итог: первая лямбда возвращает 0 (потому что была вызвана сразу, пока генератор приостановлен на i=0), вторая — 2 (потому что список построен целиком, и i завершил цикл со значением 2).
Демонстрация в коде

Для подтверждения захваченных значений можно заглянуть в __closure__:

Почему не другие варианты
- "0 0" потребовало бы, чтобы обе лямбды привязали текущее значение
iпри создании. Генераторная лямбда действительно увидит 0, но лямбды в списке видят финальное значение. - "2 0" означало бы, что генератор возвращает лямбду, которая при вызове даёт 2 — это не так, потому что мы берём лямбду из генератора сразу, пока
i == 0. - "2 2" потребовало бы, чтобы обе лямбды читали финальное значение
i(т.е. генератор был бы выполнен до конца перед next), тоже не соответствует фактическому порядку выполнения.
Как избежать "поздней привязки" (late binding)
Частая техника — фиксировать значение в аргументе по умолчанию:

Аналогично можно явно замыкать текущее значение через вспомогательную функцию:

Выводы и практический совет
- Генератор возвращает элементы по мере итерации — если взять элемент сразу, он сохранит текущее значение переменной цикла в момент
yield. - List comprehension создает все элементы сразу; все создаваемые лямбды ссылаются на одну переменную цикла, которая после цикла содержит последнее значение.
- Если нужно, чтобы каждая лямбда "захватила" текущее значение итерации, используйте аргументы по умолчанию (lambda
i=i: i) или фабричные функции.
Эта тонкость — частая ловушка при работе с замыканиями в Python; понимание порядка выполнения и области видимости избавит от неожиданных результатов.