Zmienne lokalne w funkcjach zagnieżdżonych

105

Dobra, wytrzymaj ze mną, wiem, że to będzie wyglądać strasznie zagmatwane, ale proszę, pomóż mi zrozumieć, co się dzieje.

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

Daje:

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Więc w zasadzie, dlaczego nie otrzymuję trzech różnych zwierząt? Czy nie jest cage„spakowany” w lokalnym zakresie zagnieżdżonej funkcji? Jeśli nie, w jaki sposób wywołanie funkcji zagnieżdżonej wyszukuje zmienne lokalne?

Wiem, że napotkanie tego rodzaju problemów zwykle oznacza, że ​​„robi się to źle”, ale chciałbym zrozumieć, co się dzieje.

noio
źródło
1
Spróbuj for animal in ['cat', 'dog', 'cow']... Jestem pewien, że ktoś przyjdzie i to wyjaśni - to jeden z tych Pythona gotcha :)
Jon Clements

Odpowiedzi:

114

Zagnieżdżona funkcja wyszukuje zmienne z zakresu nadrzędnego po wykonaniu, a nie po zdefiniowaniu.

Treść funkcji jest kompilowana, a zmienne „wolne” (niezdefiniowane w samej funkcji przez przypisanie) są weryfikowane, a następnie wiązane jako komórki zamykające z funkcją, a kod używa indeksu do odwoływania się do każdej komórki. pet_functionma zatem jedną wolną zmienną ( cage), do której następnie odwołuje się komórka zamykająca o indeksie 0. Samo zamknięcie wskazuje na zmienną lokalną cagew get_pettersfunkcji.

Kiedy faktycznie wywołujesz funkcję, to zamknięcie jest następnie używane do sprawdzania wartości cagew otaczającym zakresie w momencie wywołania funkcji . Tu tkwi problem. W momencie wywoływania funkcji get_pettersfunkcja jest już zakończona obliczaniem swoich wyników. cageZmiennej lokalnej w pewnym momencie, że wykonanie zostało przypisane każdej z 'cow', 'dog'i 'cat'strun, ale na końcu funkcji, cageto zawiera ostatnią wartość 'cat'. Tak więc, kiedy wywołujesz każdą z dynamicznie zwracanych funkcji, otrzymujesz 'cat'wydrukowaną wartość .

Rozwiązaniem jest nie poleganie na zamknięciach. Zamiast tego można użyć funkcji częściowej , utworzyć nowy zakres funkcji lub powiązać zmienną jako wartość domyślną parametru słowa kluczowego .

  • Przykład funkcji częściowej, używając functools.partial():

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
  • Tworzenie nowego przykładu zakresu:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Powiązanie zmiennej jako domyślnej wartości parametru słowa kluczowego:

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))

Nie ma potrzeby definiowania scoped_cagefunkcji w pętli, kompilacja odbywa się tylko raz, a nie przy każdej iteracji pętli.

Martijn Pieters
źródło
1
Waliłem dziś głową w tę ścianę przez 3 godziny nad scenariuszem do pracy. Twój ostatni punkt jest bardzo ważny i jest głównym powodem, dla którego napotkałem ten problem. Mam wiele wywołań zwrotnych z zamknięciami w całym kodzie, ale próbowanie tej samej techniki w pętli jest tym, co mnie doprowadziło.
DrEsperanto
12

Rozumiem, że klatka jest szukana w przestrzeni nazw funkcji nadrzędnej, gdy faktycznie wywoływana jest funkcja pet_function, a nie wcześniej.

Więc kiedy to zrobisz

funs = list(get_petters())

Generujesz 3 funkcje, które odnajdą ostatnio utworzoną klatkę.

Jeśli zamienisz ostatnią pętlę na:

for name, f in get_petters():
    print name + ":", 
    f()

Otrzymasz:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Nicolas Barbey
źródło
6

Wynika to z następujących

for i in range(2): 
    pass

print(i)  # prints 1

po iteracji wartość ijest leniwie przechowywana jako wartość końcowa.

Jako generator funkcja działałaby (tj. Wypisałaby każdą wartość po kolei), ale podczas przekształcania w listę przechodzi przez generator , stąd wszystkie wywołania funkcji cage( cage.animal) zwracają koty.

Andy Hayden
źródło
0

Uprośćmy pytanie. Definiować:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

Następnie, tak jak w pytaniu, otrzymujemy:

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Ale jeśli unikniemy tworzenia list()pierwszego:

>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

Co się dzieje? Dlaczego ta subtelna różnica całkowicie zmienia nasze wyniki?


Jeśli się przyjrzymy list(get_petters()), ze zmieniających się adresów pamięci jasno wynika, że ​​rzeczywiście dajemy trzy różne funkcje:

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

Jednak spójrz na elementy, cellktóre te funkcje są powiązane:

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

W przypadku obu pętli cellobiekt pozostaje taki sam przez wszystkie iteracje. Jednak, zgodnie z oczekiwaniami, specyfika, do strktórej się odnosi, różni się w drugiej pętli. cellObiektu odnosi się do animal, który tworzy się, gdy get_petters()jest tzw. Jednak animalzmienia strobiekt, do którego się odnosi, gdy działa funkcja generatora .

W pierwszej pętli podczas każdej iteracji tworzymy wszystkie fs, ale wywołujemy je dopiero po get_petters()całkowitym wyczerpaniu generatora i utworzeniu jednej listz funkcji.

W drugiej pętli, podczas każdej iteracji, zatrzymujemy get_petters()generator i dzwonimy fpo każdej przerwie. W ten sposób otrzymujemy wartość animalw tym momencie, w którym funkcja generatora jest wstrzymana.

Jak @Claudiu odpowiada na podobne pytanie :

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 animaljest zmutowany, a wszystkie zamknięcia odnoszą się do tego samego animal.

[Uwaga redaktora: izmieniono na animal.]

Mateen Ulhaq
źródło