Zakres funkcji lambda i ich parametry?

89

Potrzebuję funkcji zwrotnej, która jest prawie dokładnie taka sama dla serii zdarzeń GUI. Funkcja zachowuje się nieco inaczej w zależności od tego, które zdarzenie ją wywołało. Wydaje mi się, że to prosty przypadek, ale nie potrafię zrozumieć tego dziwnego zachowania funkcji lambda.

Więc mam następujący uproszczony kod poniżej:

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

Wynik tego kodu to:

mi
mi
mi
do
re
mi

Oczekiwałem:

do
re
mi
do
re
mi

Dlaczego użycie iteratora zepsuło sprawę?

Próbowałem użyć deepcopy:

import copy
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
    f()

Ale to ma ten sam problem.

agartland
źródło
3
Tytuł twojego pytania jest nieco mylący.
lispmachine
1
Po co używać lambd, jeśli uważasz je za mylące? Dlaczego nie użyć def do definiowania funkcji? Co jest takiego w twoim problemie, że lambdy są tak ważne?
S.Lott
@ S.Lott Zagnieżdżona funkcja spowoduje ten sam problem (może być bardziej widoczny)
lispmachine
1
@agartland: Czy jesteś mną? Ja również pracuje nad zdarzeń GUI, i napisałem następujący niemal identycznego testu przed znalezieniem tej strony podczas badań tła: pastebin.com/M5jjHjFT
imallett
5
Zobacz Dlaczego wyrażenia lambda zdefiniowane w pętli z różnymi wartościami zwracają ten sam wynik? w oficjalnym FAQ programowania w Pythonie. Całkiem ładnie wyjaśnia problem i oferuje rozwiązanie.
abarnert

Odpowiedzi:

79

Problem polega na tym, że mzmienna (odniesienie) jest pobierana z otaczającego zakresu. W zakresie lambda są przechowywane tylko parametry.

Aby rozwiązać ten problem, musisz utworzyć inny zakres dla lambda:

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

W powyższym przykładzie lambda również używa otaczającego zakresu do znalezienia m, ale tym razem jest to callback_factoryzakres, który jest tworzony raz na każde callback_factory wywołanie.

Lub z 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()
lispmachine
źródło
2
To wyjaśnienie jest nieco mylące. Problem polega na zmianie wartości m w iteracji, a nie w zakresie.
Ixx
Powyższy komentarz jest prawdziwy, jak zauważył @abarnert w komentarzu do pytania, w którym podano również link, który wyjaśnia fenonimon i rozwiązanie. Metoda fabryczna daje ten sam efekt, co argument do metody fabrycznej, tworząc nową zmienną z zakresem lokalnym dla lambda. Jednak podane rozwiązanie nie działa syntatycznie, ponieważ nie ma argumentów do lambdy - a lambda w rozwiązaniu lamda poniżej również zapewnia ten sam efekt bez tworzenia nowej trwałej metody tworzenia lambdy
Mark Parris
132

Po utworzeniu lambda nie tworzy kopii zmiennych w otaczającym zakresie, którego używa. Utrzymuje odniesienie do środowiska, dzięki czemu może później sprawdzić wartość zmiennej. Jest tylko jeden m. Jest przypisywany za każdym razem w pętli. Po pętli zmienna mma wartość 'mi'. Więc kiedy faktycznie uruchomisz funkcję, którą utworzyłeś później, wyszuka ona wartość mw środowisku, które ją stworzyło, które wtedy będzie miało wartość 'mi'.

Jednym z powszechnych i idiomatycznych rozwiązań tego problemu jest uchwycenie wartości mw momencie tworzenia lambda przez użycie jej jako domyślnego argumentu opcjonalnego parametru. Zwykle używasz parametru o tej samej nazwie, więc nie musisz zmieniać treści kodu:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))
newacct
źródło
mam na myśli opcjonalne parametry z wartościami domyślnymi
lispmachine
6
Niezłe rozwiązanie! Chociaż podstępne, wydaje mi się, że oryginalne znaczenie jest jaśniejsze niż w przypadku innych składni.
Quantum7
3
Nie ma w tym nic hakerskiego ani podstępnego; jest to dokładnie to samo rozwiązanie, które sugeruje oficjalne FAQ Pythona. Zobacz tutaj .
abarnert
3
@abernert, „hackish and tricky” niekoniecznie jest niezgodne z „byciem rozwiązaniem, które sugeruje oficjalne Python FAQ”. Dzięki za referencje.
Don Hatch
1
ponowne użycie tej samej nazwy zmiennej jest niejasne dla osoby niezaznajomionej z tą koncepcją. Ilustracja byłaby lepsza, gdyby była to lambda n = m. Tak, musiałbyś zmienić parametr wywołania zwrotnego, ale treść pętli for mogłaby pozostać taka sama, jak sądzę.
Nick
6

Python oczywiście używa referencji, ale nie ma to znaczenia w tym kontekście.

Kiedy definiujesz lambdę (lub funkcję, ponieważ jest to dokładnie to samo zachowanie), nie ocenia ona wyrażenia lambda przed uruchomieniem:

# defining that function is perfectly fine
def broken():
    print undefined_var

broken() # but calling it will raise a NameError

Jeszcze bardziej zaskakujące niż Twój przykład lambda:

i = 'bar'
def foo():
    print i

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'

Krótko mówiąc, myśl dynamicznie: nic nie jest oceniane przed interpretacją, dlatego twój kod używa najnowszej wartości m.

Kiedy szuka m w wykonaniu lambda, m jest pobierane z najwyższego zakresu, co oznacza, że, jak wskazali inni; możesz obejść ten problem, dodając kolejny zakres:

def factory(x):
    return lambda: callback(x)

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))

Tutaj, gdy wywoływana jest lambda, szuka x w zakresie definicji lambda. To x jest zmienną lokalną zdefiniowaną w treści fabryki. Z tego powodu wartość używana przy wykonywaniu lambda będzie wartością, która została przekazana jako parametr podczas wywołania fabryki. I doremi!

Uwaga, mogłem zdefiniować fabrykę jako fabrykę (m) [zamień x na m], zachowanie jest takie samo. Dla jasności użyłem innej nazwy :)

Może się okazać, że Andrej Bauer ma podobne problemy z lambdą. Ciekawe na tym blogu są komentarze, w których dowiesz się więcej o zamykaniu w Pythonie :)

Nicolas Dumazet
źródło
1

Nie jest to bezpośrednio związane z omawianym zagadnieniem, ale jest to bezcenna mądrość: Python Objects Fredrika Lundha.

tzot
źródło
1
Nie jest to bezpośrednio związane z Twoją odpowiedzią, ale wyszukiwanie kociąt: google.com/search?q=kitten
Singletoned
@Singletoned: jeśli operator operacyjny zajmie się artykułem, do którego podałem link, nie zadałby pytania w pierwszej kolejności; dlatego jest pośrednio powiązany. Jestem pewien, że z przyjemnością wyjaśnisz mi, w jaki sposób kocięta są pośrednio związane z moją odpowiedzią (zakładam, że przez podejście holistyczne;)
tzot.
1

Tak, to jest problem zasięgu, wiąże się z zewnętrznym m, niezależnie od tego, czy używasz lambda, czy funkcji lokalnej. Zamiast tego użyj funktora:

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))
Benoît
źródło
1

soluiton do lambda jest bardziej lambda

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']

zewnętrzna lambdasłuży do związania się bieżącą wartość ido j u

za każdym razem, gdy lambdawywoływane jest zewnętrzne , tworzy ono wystąpienie wewnętrznego lambdaz jpowiązaniem z bieżącą wartością ias i's

Aaron Goldman
źródło
0

Po pierwsze, to, co widzisz, nie stanowi problemu i nie jest związane z wezwaniem przez odniesienie lub wartością.

Zdefiniowana przez Ciebie składnia lambda nie ma parametrów, a zatem zakres, który widzisz z parametrem, mjest zewnętrzny w stosunku do funkcji lambda. Dlatego widzisz te wyniki.

W twoim przykładzie składnia lambda nie jest konieczna i wolałbyś raczej użyć prostego wywołania funkcji:

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

Powinieneś bardzo precyzyjnie określić, jakich parametrów lambda używasz i gdzie dokładnie zaczyna się i kończy ich zakres.

Na marginesie, jeśli chodzi o przekazywanie parametrów. Parametry w Pythonie są zawsze odniesieniami do obiektów. Cytując Alexa Martellego:

Problem terminologiczny może wynikać z faktu, że w Pythonie wartość nazwy jest odniesieniem do obiektu. Dlatego zawsze przekazujesz wartość (bez niejawnego kopiowania), a ta wartość jest zawsze referencją. [...] Teraz, jeśli chcesz wymyślić na to nazwę, na przykład „przez odniesienie do obiektu”, „według wartości nieskopiowanej” lub cokolwiek innego, bądź moim gościem. Próba ponownego użycia terminologii, która jest bardziej ogólnie stosowana w językach, w których „zmienne są ramkami”, do języka, w którym „zmienne są znacznikami post-it”, jest, IMHO, bardziej myląca niż pomocna.

Yuval Adam
źródło
0

Zmienna mjest przechwytywana, więc wyrażenie lambda zawsze widzi jej „bieżącą” wartość.

Jeśli chcesz skutecznie przechwycić wartość w danym momencie, napisz, że funkcja przyjmuje wartość, którą chcesz jako parametr, i zwraca wyrażenie lambda. W tym momencie lambda przechwyci wartość parametru , która nie zmieni się po wielokrotnym wywołaniu funkcji:

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

Wynik:

do
re
mi
Jon Skeet
źródło
0

tak naprawdę w Pythonie nie ma zmiennych w klasycznym sensie, tylko nazwy, które zostały powiązane przez odniesienia do odpowiedniego obiektu. Nawet funkcje są czymś w rodzaju obiektów w Pythonie, a lambdy nie stanowią wyjątku od reguły :)

Tomek
źródło
Kiedy mówisz „w klasycznym sensie”, masz na myśli „jak C”. Wiele języków, w tym Python, implementuje zmienne inaczej niż C.
Ned Batchelder,
0

Na marginesie, mapchociaż pogardzany przez jakąś dobrze znaną postać Pythona, wymusza konstrukcję, która zapobiega tej pułapce.

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

Uwaga: lambda iw innych odpowiedziach pierwsza zachowuje się jak fabryka.

YvesgereY
źródło