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.
python
lexical-closures
agartland
źródło
źródło
Odpowiedzi:
Problem polega na tym, że
m
zmienna (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 tocallback_factory
zakres, który jest tworzony raz na każdecallback_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()
źródło
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 zmiennam
ma wartość'mi'
. Więc kiedy faktycznie uruchomisz funkcję, którą utworzyłeś później, wyszuka ona wartośćm
w ś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
m
w 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))
źródło
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 :)
źródło
Nie jest to bezpośrednio związane z omawianym zagadnieniem, ale jest to bezcenna mądrość: Python Objects Fredrika Lundha.
źródło
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))
źródło
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
lambda
służy do związania się bieżącą wartośći
doj
uza każdym razem, gdy
lambda
wywoływane jest zewnętrzne , tworzy ono wystąpienie wewnętrznegolambda
zj
powiązaniem z bieżącą wartościąi
asi
'sźródło
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,
m
jest 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:
źródło
Zmienna
m
jest 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:
źródło
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 :)
źródło
Na marginesie,
map
chociaż 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 i
w innych odpowiedziach pierwsza zachowuje się jak fabryka.źródło