Разберите Python-фрагмент с замыканиями: funcs = []; for i in range(5): funcs.append(lambda: i); print([f() for f in funcs]) — почему все функции возвращают одно и то же значение, как это исправить и какие альтернативные конструкции избежать подобных ловушек?
Коротко — потому что замыкание захватывает переменную `i`, а не её значение; к моменту вызова всех лямбд `i` равно последнему значению цикла. В вашем примере цикл по `range(5)` даёт значения 0,1,2,3,40,1,2,3,40,1,2,3,4, но после окончания `i` равно 444, поэтому - оригинальный код funcs = [] for i in range(5): funcs.append(lambda: i) print([f() for f in funcs]) выведет [ 4,4,4,4,4 ][\;4,4,4,4,4\;][4,4,4,4,4]. Как исправить (варианты): 1) Захватить текущее значение через аргумент по умолчанию (самый простой и распространённый прием): funcs = [] for i in range(5): funcs.append(lambda i=i: i) print([f() for f in funcs]) # выведет [ 0,1,2,3,4 ][\;0,1,2,3,4\;][0,1,2,3,4] 2) Использовать фабрику замыканий: def make(x): return lambda: x funcs = [] for i in range(5): funcs.append(make(i)) 3) Использовать functools.partial (явная привязка аргумента): from functools import partial funcs = [] for i in range(5): funcs.append(partial(lambda x: x, i)) 4) Синтаксический вариант через генератор/срез: funcs = [lambda i=i: i for i in range(5)] Что стоит избегать / на что обращать внимание: - Не полагайтесь на «немедленное» запоминание значения при создании лямбд в цикле — это частая ловушка. - Будьте осторожны с захватом изменяемых объектов (списков, словарей): изменения после создания замыкания будут видны внутри него. - В Python 3 переменная цикла в списковом выражении не «протекает» наружу, но проблема позднего связывания замыкания остаётся. Вывод: либо явно связывайте значение при создании (через аргумент по умолчанию/фабрику/partial), либо избегайте создания лямбд, которые полагаются на внешний изменяющийся контекст.
- оригинальный код
funcs = []
for i in range(5):
funcs.append(lambda: i)
print([f() for f in funcs])
выведет [ 4,4,4,4,4 ][\;4,4,4,4,4\;][4,4,4,4,4].
Как исправить (варианты):
1) Захватить текущее значение через аргумент по умолчанию (самый простой и распространённый прием):
funcs = []
for i in range(5):
funcs.append(lambda i=i: i)
print([f() for f in funcs]) # выведет [ 0,1,2,3,4 ][\;0,1,2,3,4\;][0,1,2,3,4]
2) Использовать фабрику замыканий:
def make(x):
return lambda: x
funcs = []
for i in range(5):
funcs.append(make(i))
3) Использовать functools.partial (явная привязка аргумента):
from functools import partial
funcs = []
for i in range(5):
funcs.append(partial(lambda x: x, i))
4) Синтаксический вариант через генератор/срез:
funcs = [lambda i=i: i for i in range(5)]
Что стоит избегать / на что обращать внимание:
- Не полагайтесь на «немедленное» запоминание значения при создании лямбд в цикле — это частая ловушка.
- Будьте осторожны с захватом изменяемых объектов (списков, словарей): изменения после создания замыкания будут видны внутри него.
- В Python 3 переменная цикла в списковом выражении не «протекает» наружу, но проблема позднего связывания замыкания остаётся.
Вывод: либо явно связывайте значение при создании (через аргумент по умолчанию/фабрику/partial), либо избегайте создания лямбд, которые полагаются на внешний изменяющийся контекст.