Объем лямбда-функций и их параметры?
Мне нужна функция обратного вызова, которая почти точно такая же для серии событий графического интерфейса. Функция будет вести себя немного по-разному в зависимости от того, какое событие вызвало ее. Мне кажется, что это простой случай, но я не могу понять это странное поведение лямбда-функций.
Итак, у меня есть следующий упрощенный код ниже:
def callback(msg):
print msg
#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
funcList.append(lambda: callback(m))
for f in funcList:
f()
#create one at a time
funcList=[]
funcList.append(lambda: callback('do'))
funcList.append(lambda: callback('re'))
funcList.append(lambda: callback('mi'))
for f in funcList:
f()
Выход этого кода:
mi
mi
mi
do
re
mi
Я ожидал:
do
re
mi
do
re
mi
Почему использование итератора все испортило?
Я пытался использовать глубокую копию:
import copy
funcList=[]
for m in ('do', 're', 'mi'):
funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
f()
Но это та же проблема.
10 ответов
Проблема здесь заключается в m
переменная (ссылка) берется из окружающей области видимости. В области лямбды содержатся только параметры.
Чтобы решить эту проблему, вы должны создать еще одну область для лямбды:
def callback(msg):
print msg
def callback_factory(m):
return lambda: callback(m)
funcList=[]
for m in ('do', 're', 'mi'):
funcList.append(callback_factory(m))
for f in funcList:
f()
В приведенном выше примере лямбда также использует объем, чтобы найти m
, но на этот раз это callback_factory
объем, который создается один раз за каждый callback_factory
вызов.
Или с помощью functools.partial:
from functools import partial
def callback(msg):
print msg
funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
f()
Когда лямбда создается, она не копирует переменные в включенной области видимости, которую использует. Он поддерживает ссылку на среду, так что он может искать значение переменной позже. Есть только один m
, Он назначается на каждый раз через цикл. После цикла переменная m
имеет значение 'mi'
, Поэтому, когда вы на самом деле запустите функцию, которую вы создали позже, она будет искать значение m
в среде, которая его создала, которая к тому времени будет иметь значение 'mi'
,
Одним из распространенных и идиоматических решений этой проблемы является получение значения m
в то время, когда лямбда создается, используя ее в качестве аргумента по умолчанию для необязательного параметра. Обычно вы используете параметр с тем же именем, поэтому вам не нужно менять тело кода:
for m in ('do', 're', 'mi'):
funcList.append(lambda m=m: callback(m))
Конечно, Python использует ссылки, но это не имеет значения в этом контексте.
Когда вы определяете лямбду (или функцию, поскольку это точно такое же поведение), она не оценивает лямбда-выражение до времени выполнения:
# defining that function is perfectly fine
def broken():
print undefined_var
broken() # but calling it will raise a NameError
Еще более удивительно, чем ваш лямбда-пример:
i = 'bar'
def foo():
print i
foo() # bar
i = 'banana'
foo() # you would expect 'bar' here? well it prints 'banana'
Короче говоря, думайте динамически: перед интерпретацией ничего не оценивается, поэтому в вашем коде используется последнее значение m.
Когда он ищет m в лямбда-исполнении, m берется из самой верхней области, что означает, что, как указали другие; Вы можете обойти эту проблему, добавив еще одну область:
def factory(x):
return lambda: callback(x)
for m in ('do', 're', 'mi'):
funcList.append(factory(m))
Здесь, когда вызывается лямбда, он ищет в области определения лямбда для х. Этот x является локальной переменной, определенной в теле фабрики. Из-за этого значение, используемое при лямбда-выполнении, будет значением, переданным в качестве параметра во время вызова фабрики. И дореми!
Как примечание, я мог бы определить factory как factory(m) [заменить x на m], поведение такое же. Я использовал другое имя для ясности:)
Вы можете обнаружить, что у Андрея Бауэра похожие проблемы с лямбдой. Что интересно в этом блоге, так это комментарии, где вы узнаете больше о закрытии Python:)
Не имеет прямого отношения к рассматриваемой проблеме, но тем не менее бесценная мудрость: Python Objects от Fredrik Lundh.
Да, это проблема области видимости, она привязывается к внешнему m независимо от того, используете ли вы лямбду или локальную функцию. Вместо этого используйте функтор:
class Func1(object):
def __init__(self, callback, message):
self.callback = callback
self.message = message
def __call__(self):
return self.callback(self.message)
funcList.append(Func1(callback, m))
Решение лямбда больше лямбда
In [0]: funcs = [(lambda j: (lambda: j))(i) for i in ('do', 're', 'mi')]
In [1]: funcs
Out[1]:
[<function __main__.<lambda>>,
<function __main__.<lambda>>,
<function __main__.<lambda>>]
In [2]: [f() for f in funcs]
Out[2]: ['do', 're', 'mi']
внешний lambda
используется для привязки текущего значения i
в j
на
каждый раз внешний lambda
называется это делает экземпляр внутреннего lambda
с j
привязан к текущей стоимости i
как i
ценность
Как примечание стороны, map
, хотя презираемый некоторыми хорошо известными фигурами Python, создает конструкцию, которая предотвращает эту ловушку.
fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])
NB: первый lambda i
действует как фабрика в других ответах.
На самом деле в Python нет переменных в классическом смысле, только имена, связанные ссылками на соответствующий объект. Даже функции - это какой-то объект в Python, и лямбды не делают исключения из правила:)
Переменная m
захватывается, поэтому ваше лямбда-выражение всегда видит свое "текущее" значение.
Если вам необходимо эффективно зафиксировать значение в определенный момент времени, напишите функцию, которая принимает значение, которое вы хотите, в качестве параметра и возвращает лямбда-выражение. В этот момент лямбда будет захватывать значение параметра, которое не изменится при многократном вызове функции:
def callback(msg):
print msg
def createCallback(msg):
return lambda: callback(msg)
#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
funcList.append(createCallback(m))
for f in funcList:
f()
Выход:
do
re
mi
Во-первых, то, что вы видите, не является проблемой и не связано с вызовом по ссылке или по значению.
Лямбда-синтаксис, который вы определили, не имеет параметров, и поэтому область действия, которую вы видите с параметром m
является внешним по отношению к лямбда-функции. Вот почему вы видите эти результаты.
Лямбда-синтаксис, в вашем примере не является необходимым, и вы бы предпочли использовать простой вызов функции:
for m in ('do', 're', 'mi'):
callback(m)
Опять же, вы должны быть очень точными в том, какие параметры лямбды вы используете, и где именно их область начинается и заканчивается.
В качестве примечания по поводу передачи параметров. Параметры в Python всегда являются ссылками на объекты. Цитирую Алекса Мартелли:
Проблема терминологии может быть связана с тем, что в python значение имени является ссылкой на объект. Таким образом, вы всегда передаете значение (без неявного копирования), и это значение всегда является ссылкой. [...] Теперь, если вы хотите пометить имя для этого, например, "по ссылке на объект", "по неопечатанному значению", или как угодно, будьте моим гостем. Попытка повторно использовать терминологию, которая в целом применяется к языкам, где "переменные являются блоками", к языку, где "переменные являются тегами post-it", скорее всего, сбивает с толку, чем помогает.