Замыкания и "late binding" в Python: почему вывод [4, 4, 4]

Замыкания и "late binding" в Python: почему вывод [4, 4, 4]

@python_quiz

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

Исходный фрагмент:

python

Коротко — при выполнении получится [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 ссылаться на эту переменную и испытывать эффект поздней привязки.

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

python

Как получить ожидаемый (различный) результат

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

1) Захват значения через аргумент по умолчанию

python

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

2) Фабрика (функция-замыкание)

python

Каждому вызову make_mul создаётся своя локальная переменная j, и лямбда замыкает именно её.

3) functools.partial или оператор multiplication (альтернативный вариант)

python

partial связывает аргумент в момент создания, фиксируя значение.

Частые ошибки и пояснения

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

Выводы

  • Лямбды и функции в Python замыкают переменные по ссылке; итоговое значение определяется во время вызова, а не во время создания.
  • Чтобы зафиксировать текущее значение переменной при создании замыкания, используйте аргументы по умолчанию или фабрики функций.
  • Понимание этой особенности убережёт от неожиданных результатов и ошибок в коде, особенно при создании коллекций функций в циклах.

Report Page