среда, 15 октября 2008 г.

Неосторожность с lambda

Все утро боролся с кодом, в котором вроде как все правильно, но работал не так как надо. Выглядел он примерно так:

Но результат выполнения этого кода получился неожиданный:

item a result c
item c result c
item b result c


Проблема оказалась в строке:
m.addAction(item, lambda : item)

lambda использует переменную item из контекста вызывающей функции. Переменная изменяется в цикле и к моменту вызова lambda в item сохраняется значение последней итерации.

Отсюда вывод - смешивание функционального подхода и "обычного" может привести к самым неожиданным результатам. Для того, что бы избежать такого смешивания, достаточно заменить цикл на map:
map(lambda item: m.addAction(item, lambda : item), lst)

8 комментариев:

bialix комментирует...

месье знает толк в извращениях

sash_ko комментирует...

да разве это извращения? :)

Анонимный комментирует...

А если вот так :-):
[m.addAction(item, lambda: item) for item in lst]?
Вообще-то map тоже содержит цикл, только неявный, скрытый в самой функции, так что отказаться от циклов не получилось, по-моему.
Как насчёт рекурсии?

sash_ko комментирует...

>> [m.addAction(item, lambda: item) for item in lst]

так к сожалению не получится. тут дело скорее не в цикле, так как если пример немного подправить, то все заработает как надо:

fn = lambda item: m.addAction(item, lambda : item)
[fn(item) for item in lst]

мне кажется, тут дело в окружении, в котором создается lambda. в последнем случае имеет создается 2 lambdы: fn и результат выполнения fn(). во втором случае окружением для lambda является внутренности fn, а там только одно значение item, так как в python параметры передаются в функцию по значению. тоже самое происходит с map.

не знаю, насколько такое объяснение правильно, надо будет еще подумать :)

Анонимный комментирует...

Я из вредности поковырял этот первоначальный код. Вообще-то я бы выделил как источник всех бед именно цикл:
for item in lst:
m.addAction(item,lambda:item)
Попробуем добавить вот это:
for item in ['d']:
m.addAction(item,lambda:item)
Результат столь же печален:
item a result d
item b result d
item c result d
item d result d
А теперь попробуем этот код:
for it in ['d']:
m.addAction(it,lambda:it)
item a result c
item b result c
item c result c
item d result d

Получается, после применения цикла python создает переменную item, причём глобальную? И действительно: после запуска globals()['item'] возвращает нам ее значение. Я в ужасе.
Теперь понятно, что из-за того, что имена в python это всего лишь ссылки на объекты, то как только при новой итерации меняется объект, на который ссылается item, меняются и результаты, возвращаемые lambda: item

Подытоживая, скажу, что "lambda: item" попросту опасно применять. :-)
K

sash_ko комментирует...

Цикл создает глобальную переменную, если он сам находится в глобальном пространстве модуля (это вроде не совсем по питоновски звучит, но, думаю, понятно). Если его переместить в функцию:

def fill():
for item in lst:
m.addAction(item,lambda:item)

То все станет на свои места:
globals() после fill() не покажет item, а locals() в fill() - покажет.

А вот с тем, что результат lambda: item меняется с изменением item в цикле (они ссылаются на один и тот же объект) согласен. Именно поэтому спасает map или "вложенная" lambda.

В изначальном примере, lambda - это отложенная операция с объектом, изменялся в цикле. Раз операция отложенная, то после завершения цикла она будет применяться к последнему значению. Для того, что бы этого избежать, нужно получить копию объекта. Для этого, создание lambda оборачивается вызовом функции в теле цикла, при этом создается копия объекта, на который ссылается item. lambda использует item из области видимости, в которой создается. Если эта область - оборачивающая функция с копией item, то все проблемы исчезают :)

Анонимный комментирует...

Согласен, в свою очередь, с вашими аргументами. Но мне всё-таки не нравится тот факт, что захламляется глобальное пространство имен. Не тру, на мой взгляд. :)
Немного не в тему, но не читали ли Вы статьи David'а Merz'а на тему функционального стиля программирования в python? Они, правда, уже немного староваты, но не стали менее интересными от этого.

sash_ko комментирует...

Все таки захламляется не все пространство имен, а только в пределах функции. Мне кажется именно из-за этого возможны такие штуки как comprehensions, что довольно удобно. В захламлении есть свои прелести :)

David'а Merz'а читал когда начал изучать python, но в то время я думал c++'ом, не все понял и не оценил. Спасибо что напомнили, надо в ближайшее время перечитать, сейчас это скорее всего воспримется по другому.