Сфера застосування лямбда-функцій та їх параметрів?


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

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

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

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

5
Див. Чому лямбди, визначені в циклі з різними значеннями, повертають однаковий результат? в офіційних поширених запитаннях щодо програмування для 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 у коментарі до питання, де також дається посилання, що пояснює фенонімон та рішення. Заводський метод забезпечує той самий ефект, що і аргумент фабричного методу, створює нову змінну з локальним для лямбда-зони обсягом. Однак наданий розв'язок не працює синтаксично, оскільки відсутні аргументи для лямбди - а лямбда в розчині lamda нижче також забезпечує той самий ефект, не створюючи нового стійкого методу для створення лямбда
Марк Парріс

132

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

Одним із загальних та ідіоматичних рішень цієї проблеми є фіксація значення mчасу, коли створюється лямбда, використовуючи його як аргумент за замовчуванням необов’язкового параметра. Зазвичай ви використовуєте однойменний параметр, тому вам не доведеться міняти тіло коду:

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

я маю на увазі необов’язкові параметри зі значеннями за замовчуванням
lispmachine

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

3
У цьому немає нічого шаленого чи хитрого; це абсолютно те саме рішення, яке пропонують офіційні поширені запитання щодо Python. Дивіться тут .
abarnert

3
@abernert, "хакерський і хитрий" не обов'язково несумісний із "тим рішенням, яке пропонують офіційні поширені запитання щодо Python". Дякую за посилання.
Дон Хетч,

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 - локальна змінна, визначена в тілі заводу. Через це значення, яке використовується при виконанні лямбда-сигналу, буде значенням, яке було передано як параметр під час виклику до заводу. І доремі!

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

Ви можете виявити, що Андрій Бауер мав подібні лямбда-проблеми. Що цікаво в цьому блозі - це коментарі, де ви дізнаєтесь більше про закриття python :)


1

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


1
Не має прямого відношення до вашої відповіді, але пошук кошенят: google.com/search?q=kitten
Одинокий

@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прив'язується до поточного значення , iяк iзначення «S


0

По-перше, те, що ви бачите, не є проблемою і не пов’язане із викликом за посиланням чи за значенням.

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

Лямбда-синтаксис у вашому прикладі не потрібен, і ви скоріше використовуєте простий виклик функції:

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

Знову ж таки, ви повинні бути дуже точними щодо того, які лямбда-параметри ви використовуєте, і де саме починається і закінчується їх область дії.

Як додаткове зауваження щодо передачі параметрів. Параметри в python - це завжди посилання на об'єкти. Процитувавши Алекса Мартеллі:

Проблема термінології може бути пов’язана з тим, що в python значення імені є посиланням на об’єкт. Отже, ви завжди передаєте значення (без неявного копіювання), і це значення завжди є посиланням. [...] Тепер, якщо ви хочете назвати ім’я для цього, наприклад, «за посиланням на об’єкт», «за не скопійованим значенням» чи будь-яке інше, будьте моїм гостем. Спроба повторно використати термінологію, яка в більшій мірі застосовується до мов, де "змінні - це поля", до мови, де "змінні - це теги post-it", 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хоча і зневажається деякими відомими діячами Пітона, змушує споруду, яка запобігає цій пастці.

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

Примітка. Перша lambda iдія відповідає фабриці в інших відповідях.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.