Почему генератор списка внутри функции ведёт себя неожиданно?
Странное поведение генератора списково def make_multipliers(): return [lambda x: x * i for i in range(3)]
for f in make_multipliers(): print(f(2)) Ожидал увидеть 0, 2, 4, а получил 4, 4, 4. Я знаю про позднее связывание, но как это починить красиво?
1 ответ
Это классическая ловушка позднего связывания (late binding), и ты прав, что дело в нём — давай прицельно, почему так и как починить красиво.
Лямбды lambda x: x * i не запоминают значение i в момент создания. Они запоминают саму переменную i — ссылку на неё. К моменту, когда ты их вызываешь, цикл уже закончился, и i во всех трёх лямбдах указывает на одно и то же последнее значение — 2. Отсюда и 4, 4, 4.
Чинится «заморозкой» текущего i в момент создания лямбды. Самый чистый способ — аргумент по умолчанию, он вычисляется сразу:
def make_multipliers():
return [lambda x, i=i: x * i for i in range(3)]
Теперь у каждой лямбды своя копия i, и вывод — 0, 2, 4, как ты и ждал.
Если не хочется «лишнего» параметра — functools.partial:
from functools import partial
def make_multipliers():
return [partial(lambda i, x: x * i, i) for i in range(3)]
Но i=i читается проще, и это самый ходовой приём. Та же ловушка живёт и в замыканиях с обычным def внутри цикла, не только в лямбдах — принцип один.