Замыкания и лямбды в Python: почему вывод [2, 2, 2]

Замыкания и лямбды в Python: почему вывод [2, 2, 2]

@python_quiz

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

Код квиза:

python

Краткий ответ — все три лямбды возвращают одно и то же значение: [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):

python

Вы увидите, что все лямбды ссылаются на одну и ту же ячейку памяти с содержимым 2.

Как получить [0, 1, 2] — способы привязать текущее значение

1) Использовать значение по умолчанию аргумента функции (самый распространённый и краткий прием):

python

Аргумент i=i создаёт новую локальную переменную со значением текущего i в момент создания лямбды.

2) Использовать фабрику функций (явно создать замыкание с собственной переменной):

python

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

python

Все эти подходы создают для каждой функции своё собственное значение, вместо ссылки на общую изменяемую переменную.

Анализ вариантов ответа (коротко)

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

Вывод

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

Report Page