Podczas badania problemu, który miałem z zamknięciami leksykalnymi w kodzie JavaScript, napotkałem ten problem w Pythonie:
flist = []
for i in xrange(3):
def func(x): return x * i
flist.append(func)
for f in flist:
print f(2)
Zauważ, że ten przykład świadomie unika lambda
. Drukuje "4 4 4", co jest zaskakujące. Spodziewałbym się „0 2 4”.
Ten równoważny kod w Perlu robi to dobrze:
my @flist = ();
foreach my $i (0 .. 2)
{
push(@flist, sub {$i * $_[0]});
}
foreach my $f (@flist)
{
print $f->(2), "\n";
}
Drukowane jest "0 2 4".
Czy możesz wyjaśnić różnicę?
Aktualizacja:
Problem nie jest z i
bycia globalnym. To wyświetla to samo zachowanie:
flist = []
def outer():
for i in xrange(3):
def inner(x): return x * i
flist.append(inner)
outer()
#~ print i # commented because it causes an error
for f in flist:
print f(2)
Jak pokazuje skomentowana linia, i
w tym momencie nie jest znana. Mimo to wypisuje "4 4 4".
python
closures
lazy-evaluation
late-binding
Eli Bendersky
źródło
źródło
Odpowiedzi:
Python faktycznie zachowuje się zgodnie z definicją. Tworzone są trzy oddzielne funkcje , ale każda z nich ma zamknięcie środowiska, w którym została zdefiniowana - w tym przypadku środowisko globalne (lub środowisko funkcji zewnętrznej, jeśli pętla jest umieszczona wewnątrz innej funkcji). W tym jednak dokładnie tkwi problem - w tym środowisku i jest zmutowane , a wszystkie zamknięcia odnoszą się do tego samego i .
Tutaj jest najlepszym rozwiązaniem mogę wymyślić - stworzyć creater funkcji i wywołać że zamiast. Wymusi to różne środowiska dla każdej z utworzonych funkcji, z innym i w każdej z nich.
Tak dzieje się, gdy łączysz efekty uboczne i programowanie funkcjonalne.
źródło
def inner(x, i=i): return x * i
Funkcje zdefiniowane w pętli mają dostęp do tej samej zmiennej
i
podczas zmiany jej wartości. Na końcu pętli wszystkie funkcje wskazują na tę samą zmienną, która przechowuje ostatnią wartość w pętli: efektem jest to, co podano w przykładzie.Aby ocenić
i
i wykorzystać jego wartość, powszechnym wzorcem jest ustawienie go jako domyślnego parametru: wartości domyślne parametrów są oceniane, gdydef
instrukcji, a tym samym wartość zmiennej pętli jest zamrażana.Następujące działa zgodnie z oczekiwaniami:
źródło
def
instrukcji /i
z definicji. :-(Oto, jak to zrobić, korzystając z
functools
biblioteki (która nie jestem pewien, była dostępna w momencie zadawania pytania).Wyjścia 0 2 4, zgodnie z oczekiwaniami.
źródło
functools.partialmethod()
od Pythona 3.4Spójrz na to:
Oznacza to, że wszystkie wskazują na tę samą instancję zmiennej i, która po zakończeniu pętli będzie miała wartość 2.
Czytelne rozwiązanie:
źródło
Dzieje się tak, że zmienna i jest przechwytywana, a funkcje zwracają wartość, do której jest przypisana w momencie wywołania. W językach funkcjonalnych taka sytuacja nigdy się nie zdarza, ponieważ nie dałbym rady. Jednak w przypadku Pythona, a także, jak widzieliście w przypadku seplenienia, nie jest to już prawdą.
Różnica w stosunku do twojego przykładu schematu polega na semantyce pętli do. Scheme efektywnie tworzy nową zmienną i za każdym razem w pętli, zamiast ponownie używać istniejącego wiązania i, tak jak w przypadku innych języków. Jeśli użyjesz innej zmiennej utworzonej poza pętlą i zmienisz ją, zobaczysz to samo zachowanie w schemacie. Spróbuj zamienić pętlę na:
Spójrz tutaj, aby dowiedzieć się więcej na ten temat.
[Edytuj] Prawdopodobnie lepszym sposobem opisania tego jest myślenie o pętli do jako o makrze, które wykonuje następujące kroki:
to znaczy. odpowiednik poniższego pythona:
I nie jest już tą z zakresu nadrzędnego, ale zupełnie nową zmienną w swoim własnym zakresie (tj. Parametr do lambda), więc otrzymujesz obserwowane zachowanie. Python nie ma tego niejawnego nowego zakresu, więc treść pętli for po prostu współużytkuje zmienną i.
źródło
Nadal nie jestem do końca przekonany, dlaczego w niektórych językach działa to w jeden, a w inny sposób. W Common Lisp jest jak Python:
Wyświetla "6 6 6" (zwróć uwagę, że tutaj lista jest od 1 do 3 i jest zbudowana w odwrotnej kolejności). W Scheme działa jak w Perlu:
Wyświetla „6 4 2”
Jak już wspomniałem, Javascript znajduje się w obozie Python / CL. Wygląda na to, że podjęto tutaj decyzję wdrożeniową, w której różne języki podchodzą na różne sposoby. Bardzo chciałbym dokładnie zrozumieć, jaka jest decyzja.
źródło
Problem polega na tym, że wszystkie funkcje lokalne wiążą się z tym samym środowiskiem, a więc z tą samą
i
zmienną. Rozwiązaniem (obejściem) jest utworzenie oddzielnych środowisk (ramek stosu) dla każdej funkcji (lub lambda):źródło
Zmienna
i
jest globalna, której wartość wynosi 2 za każdym razem, gdy funkcjaf
.Byłbym skłonny zaimplementować zachowanie, którego szukasz, w następujący sposób:
Odpowiedź na twoją aktualizację : To nie globalność
i
per se powoduje to zachowanie, to fakt, że jest to zmienna z otaczającego zakresu, która ma stałą wartość w czasie wywołania f. W drugim przykładzie wartośći
jest pobierana z zakresukkk
funkcji i nic tego nie zmienia, gdy wywołujesz funkcjeflist
.źródło
Powód tego zachowania został już wyjaśniony i opublikowano wiele rozwiązań, ale myślę, że jest to najbardziej Pythonowe (pamiętaj, wszystko w Pythonie jest obiektem!):
Odpowiedź Claudiu jest całkiem dobra, używając generatora funkcji, ale odpowiedź piro to hack, szczerze mówiąc, ponieważ zmienia i w „ukryty” argument z wartością domyślną (będzie działać dobrze, ale nie jest „pythonowy”) .
źródło
func
Nax * func.i
zawsze odnoszą się do ostatniej funkcji zdefiniowanej. Więc nawet jeśli każda funkcja z osobna ma przypięty prawidłowy numer, i tak wszystkie kończą czytanie od ostatniej.