Почему выражение с lambda и comprehensions выводит "0 2"

Почему выражение с lambda и comprehensions выводит "0 2"

@python_quiz

Разберем этот квиз

Кратко: код

python

выводит

Ниже подробно разберём, почему так, и как это поведение воспроизвести и избежать.

Что делает выражение — разбиваем по частям

Есть две части:

  1. next((lambda: i for i in range(3)))()

- Создаётся генераторное выражение, которое в цикле for i in range(3) порождает объекты lambda: i.
- Вызов next(...) выполняет генератор до первого yield и возвращает первую сгенерированную лямбду. При этом генератор приостановлен на первом шаге и переменная i в его фрейме равна 0. Вызов полученной лямбды сразу же читает текущее значение i — это 0.

  1. [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).

Демонстрация в коде

python

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

python

Почему не другие варианты

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

Как избежать "поздней привязки" (late binding)

Частая техника — фиксировать значение в аргументе по умолчанию:

python

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

python

Выводы и практический совет

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

Эта тонкость — частая ловушка при работе с замыканиями в Python; понимание порядка выполнения и области видимости избавит от неожиданных результатов.

Report Page