Объем лямбда-функций и их параметры? - PullRequest
74 голосов
/ 02 июня 2009

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

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

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 ]

117 голосов
/ 02 июня 2009

Когда создается лямбда, она не создает копию переменных в используемой области видимости. Он поддерживает ссылку на среду, так что он может искать значение переменной позже. Есть только один m. Он назначается на каждый раз через цикл. После цикла переменная m имеет значение 'mi'. Поэтому, когда вы на самом деле запустите функцию, созданную вами позже, она будет искать значение m в среде, которая ее создала, которая к тому времени будет иметь значение 'mi'.

Одним из распространенных и идиоматических решений этой проблемы является захват значения m во время создания лямбды с использованием его в качестве аргумента по умолчанию для необязательного параметра. Обычно вы используете параметр с тем же именем, поэтому вам не нужно менять тело кода:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))
66 голосов
/ 02 июня 2009

Проблема здесь в том, что переменная 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()
5 голосов
/ 02 июня 2009

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 является локальной переменной, определенной в теле фабрики. Из-за этого значение, используемое при лямбда-выполнении, будет значением, переданным в качестве параметра во время вызова фабрики. И дореми!

Как примечание, я мог бы определить фабрику как фабрику (m) [заменить x на m], поведение такое же. Я использовал другое имя для ясности:)

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

1 голос
/ 05 июня 2009

Не имеет прямого отношения к рассматриваемой проблеме, но тем не менее бесценная мудрость: Объекты Python Фредрика Лунда.

0 голосов
/ 14 июля 2017

больше лямбда в раствор лямбда

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

0 голосов
/ 07 сентября 2012

Как примечание стороны, map, хотя презирается какой-то хорошо известной фигурой Python, вызывает конструкцию, которая предотвращает эту ловушку.

fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])

Примечание: первый lambda i действует как фабрика в других ответах.

0 голосов
/ 02 июня 2009

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

0 голосов
/ 02 июня 2009

Да, это проблема области видимости, она связана с внешним 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))
0 голосов
/ 02 июня 2009

Переменная 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
0 голосов
/ 02 июня 2009

Во-первых, то, что вы видите, не является проблемой и не связано с вызовом по ссылке или по значению.

Синтаксис лямбда, который вы определили, не имеет параметров, и поэтому область действия, которую вы видите с параметром m, является внешней по отношению к лямбда-функции. Вот почему вы видите эти результаты.

Лямбда-синтаксис, в вашем примере нет необходимости, и вы бы предпочли использовать простой вызов функции:

for m in ('do', 're', 'mi'):
    callback(m)

Опять же, вы должны быть очень точными в том, какие параметры лямбды вы используете и где именно их область действия начинается и заканчивается.

В качестве примечания относительно передачи параметров. Параметры в Python всегда являются ссылками на объекты. Цитируя Алекса Мартелли:

Проблема терминологии может быть связана с тот факт, что в Python значение имя - это ссылка на объект. Таким образом, вы всегда передаете значение (нет неявное копирование), и это значение всегда ссылка. [...] Теперь, если вы хочу присвоить себе имя, например: "по ссылке на объект", "не скопировано ценность ", или что-нибудь, будь моим гостем. Попытка повторно использовать терминологию, которая в целом применяется к языкам где «переменные - это коробки» для язык, где "переменные пост-это теги "это, ИМХО, больше шансов перепутать чем помочь.

...