Ловушка замыканий: Почему ваши лямбды в цикле сломаны (Late Binding) Вы пишете код, который генерирует список функций (например, колбэки для кнопок в …
Вы пишете код, который генерирует список функций (например, колбэки для кнопок в UI или динамические фильтры). Кажется, что все логично, но на выходе получаете сюрприз.
Проблемный код:
# Хотим создать 3 функции, которые возвращают 0, 1 и 2 соответственно
funcs = []
for i in range(3):
funcs.append(lambda: i)
# Проверяем
results = [f() for f in funcs]
print(results)
# Ожидание: [0, 1, 2]
# Реальность: [2, 2, 2]
Почему так происходит?
Это называется Late Binding (позднее связывание).
В Python замыкания (closures) захватывают переменные по ссылке, а не по значению.
Когда вы объявляете lambda: i, Python не сохраняет текущее число 0, 1 или 2. Он сохраняет инструкцию: «когда меня вызовут, пойди в локальную область видимости, найди переменную с именем i и возьми ее значение».
К моменту, когда вы начинаете вызывать функции из списка results, цикл for уже завершился. Переменная i в этой области видимости навсегда осталась равной 2. Все три лямбды смотрят на одну и ту же переменную i.
Как лечить?
Есть два каноничных способа заставить Python запомнить значение «здесь и сейчас».
1. Аргумент по умолчанию (Hack way)
Значения аргументов по умолчанию вычисляются в момент определения функции.
funcs = []
for i in range(3):
# i=i создает локальную переменную i внутри функции
# и присваивает ей текущее значение i из цикла
funcs.append(lambda i=i: i)
Это работает быстро, но выглядит немного грязно и может сбить с толку линтеры или коллег.
2. functools.partial (Enterprise way)
Более чистый и явный способ. partial создает новый callable-объект, «замораживая» переданные аргументы.
from functools import partial
funcs = []
for i in range(3):
# Здесь значение i фиксируется жестко
funcs.append(partial(lambda x: x, i))
Где это стреляет в реальной жизни?
- Генерация command для кнопок в Tkinter/PyQt.
- Динамическое создание task в asyncio циклах.
- Патчинг тестов в циклах.
Не дайте переменным пережить свое время.
#python #internals #functionalprogramming #gotchas
👉 @BookPython