Объем лямбда-функций и их параметры?

Мне нужна функция обратного вызова, которая почти точно такая же для серии событий графического интерфейса. Функция будет вести себя немного по-разному в зависимости от того, какое событие вызвало ее. Мне кажется, что это простой случай, но я не могу понять это странное поведение лямбда-функций.

Итак, у меня есть следующий упрощенный код ниже:

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", скорее всего, сбивает с толку, чем помогает.

Другие вопросы по тегам