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


89

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

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

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()

Но здесь та же проблема.


3
Название вашего вопроса несколько вводит в заблуждение.
lispmachine 02

1
Зачем использовать лямбды, если они вас сбивают с толку? Почему бы не использовать def для определения функций? Что в вашей проблеме делает лямбды такими важными?
S.Lott

@ S.Lott Вложенная функция приведет к той же проблеме (возможно, более четко видно)
lispmachine

1
@agartland: Ты меня? Я тоже работал над событиями графического интерфейса, и я написал следующий почти идентичный тест, прежде чем найти эту страницу во время фонового исследования: pastebin.com/M5jjHjFT
imallett

5
См. Почему лямбды, определенные в цикле с разными значениями, возвращают один и тот же результат? в официальном FAQ по программированию для Python. Он довольно хорошо объясняет проблему и предлагает решение.
abarnert

Ответы:


79

Проблема здесь в том, что 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()

2
Это объяснение немного вводит в заблуждение. Проблема заключается в изменении значения m в итерации, а не в области действия.
Ixx

Комментарий выше, это правда, как отметил @abarnert в комментарии к вопросу, где также дана ссылка, объясняющая феноним и решение. Заводской метод дает тот же эффект, что и аргумент заводского метода создает новую переменную с областью действия, локальной для лямбда-выражения. Однако данное решение не работает синтаксически, поскольку для лямбда нет аргументов - и лямбда в решении лямбда ниже также обеспечивает тот же эффект без создания нового постоянного метода для создания лямбда
Марк Пэррис,

132

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

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

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))

я имею в виду необязательные параметры со значениями по умолчанию
lispmachine 02

6
Хорошее решение! Хотя это и сложно, я чувствую, что исходное значение яснее, чем с другими синтаксисами.
Quantum7

3
В этом нет ничего хакерского или хитрого; это точно такое же решение, которое предлагает официальный Python FAQ. Смотрите здесь .
abarnert

3
@abernert, «хакерский и хитрый» не обязательно несовместим с «решением, которое предлагает официальный Python FAQ». Спасибо за ссылку.
Дон Хэтч,

1
повторное использование того же имени переменной непонятно для тех, кто не знаком с этой концепцией. Иллюстрация была бы лучше, если бы это была лямбда n = m. Да, вам придется изменить свой параметр обратного вызова, но я думаю, что тело цикла for может остаться прежним.
Ник

6

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

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

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


1

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


1
Не имеет прямого отношения к вашему ответу, но поиск котят: google.com/search?q=kitten
Singletoned

@Singletoned: если бы ОП отнесся к статье, на которую я дал ссылку, они бы вообще не задавали вопрос; поэтому это косвенно связано. Я уверен, вы с удовольствием объясните мне, как котята косвенно связаны с моим ответом (полагаю, посредством целостного подхода;)
tzot

1

Да, это проблема области видимости, она привязана к внешнему 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))

1

решение лямбда больше лямбда

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привязкой к текущему значению ias ivalue


0

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

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

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

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

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

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

Проблема с терминологией может быть связана с тем, что в Python значение имени является ссылкой на объект. Таким образом, вы всегда передаете значение (без неявного копирования), и это значение всегда является ссылкой. [...] Теперь, если вы хотите придумать для этого имя, например, «по объектной ссылке», «по нескопированному значению» или как-то еще, будь моим гостем. Попытка повторно использовать терминологию, которая более широко применяется к языкам, где «переменные - это блоки», к языку, где «переменные - это теги», IMHO, скорее запутает, чем поможет.


0

Переменная 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

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


Когда вы говорите «в классическом смысле», вы имеете в виду «как у C». Многие языки, включая Python, реализуют переменные иначе, чем C.
Нед Батчелдер

0

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

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

NB: первый lambda iдействует как завод в других ответах.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.