Короткий ответ
Замыкание — это функция, которая запоминает переменные из окружающей (объемлющей) области видимости даже после завершения выполнения этой области; при создании нескольких замыканий в цикле они обычно ссылаются на одну и ту же переменную цикла по имени, а не на её значение на момент создания, поэтому все замыкания видят её финальное значение.
Как это работает подробнее
Внутренняя функция хранит ссылку на ячейку переменной (cell) во внешней функции, а не копию значения на момент определения. Переменная цикла for i in range(n) — это одна и та же переменная на всех итерациях, поэтому все замыкания, созданные в цикле и ссылающиеся на i, после завершения цикла видят её последнее значение.
Пример кода
example.py
handlers = []
for i in range(3):
handlers.append(lambda: i) # все лямбды делят одну переменную i
print([h() for h in handlers]) # [2, 2, 2] — неожиданно одинаково
handlers_fixed = []
for i in range(3):
handlers_fixed.append(lambda i=i: i) # захватываем текущее значение i
print([h() for h in handlers_fixed]) # [0, 1, 2] — как и ожидалосьЧто использовать на практике
- Практика. Проблема решается "захватом" текущего значения через параметр по умолчанию (
def f(i=i): ...) или черезfunctools.partial, который фиксирует значение аргумента в момент создания. Замыкания широко используют для фабрик функций, мемоизации и для реализации декораторов с параметрами.
Подводные камни
- Риски. Создают список обработчиков/лямбд в цикле без захвата текущего значения переменной и получают одинаковое поведение у всех, хотя ожидали разное. Также не осознают, что это не баг Python, а последствие того, как работают области видимости и позднее связывание (late binding) имён.