Variabili locali in funzioni annidate


105

Ok, abbi pazienza su questo, so che sembrerà orribilmente contorto, ma per favore aiutami a capire cosa sta succedendo.

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

dà:

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

Quindi, fondamentalmente, perché non ricevo tre animali diversi? Il cage"pacchetto" non è nell'ambito locale della funzione annidata? In caso contrario, come fa una chiamata alla funzione annidata a cercare le variabili locali?

So che imbattersi in questo tipo di problemi di solito significa che si sta "facendo male", ma mi piacerebbe capire cosa succede.


1
Prova for animal in ['cat', 'dog', 'cow']... sono sicuro che qualcuno verrà e spiegherà questo però - è uno di quei gotcha di Python :)
Jon Clements

Risposte:


114

La funzione annidata cerca le variabili dall'ambito padre quando viene eseguita, non quando viene definita.

Il corpo della funzione viene compilato e le variabili "libere" (non definite nella funzione stessa dall'assegnazione) vengono verificate, quindi legate come celle di chiusura alla funzione, con il codice che utilizza un indice per fare riferimento a ciascuna cella. pet_functionha quindi una variabile libera ( cage) a cui viene fatto riferimento tramite una cella di chiusura, indice 0. La chiusura stessa punta alla variabile locale cagenella get_pettersfunzione.

Quando si chiama effettivamente la funzione, tale chiusura viene quindi utilizzata per esaminare il valore di cagenell'ambito circostante nel momento in cui si chiama la funzione . Qui sta il problema. Nel momento in cui chiamate le vostre funzioni, la get_pettersfunzione ha già calcolato i suoi risultati. La cagevariabile locale ad un certo punto durante l'esecuzione che è stato assegnato ciascuno dei 'cow', 'dog'e 'cat'stringhe, ma alla fine della funzione, cagecontiene tale ultimo valore 'cat'. Pertanto, quando si chiama ciascuna delle funzioni restituite dinamicamente, si ottiene il valore 'cat'stampato.

La soluzione è non fare affidamento sulle chiusure. È invece possibile utilizzare una funzione parziale , creare un nuovo ambito di funzione o associare la variabile come valore predefinito per un parametro di parola chiave .

  • Esempio di funzione parziale, utilizzando 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)))
  • Creazione di un nuovo esempio di ambito:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Associazione della variabile come valore predefinito per un parametro di parola chiave:

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

Non è necessario definire la scoped_cagefunzione nel ciclo, la compilazione avviene solo una volta, non ad ogni iterazione del ciclo.


1
Oggi ho sbattuto la testa su questo muro per 3 ore su un copione per lavoro. Il tuo ultimo punto è molto importante ed è il motivo principale per cui ho riscontrato questo problema. Ho callback con chiusure in abbondanza in tutto il mio codice, ma provare la stessa tecnica in un ciclo è ciò che mi ha colpito.
DrEsperanto

12

La mia comprensione è che la gabbia viene cercata nello spazio dei nomi della funzione padre quando viene effettivamente chiamata la funzione pet_funzione prodotta, non prima.

Quindi quando lo fai

funs = list(get_petters())

Si generano 3 funzioni che troveranno l'ultima gabbia creata.

Se sostituisci il tuo ultimo ciclo con:

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

Otterrai effettivamente:

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

6

Ciò deriva da quanto segue

for i in range(2): 
    pass

print(i)  # prints 1

dopo aver ripetuto il valore di i viene memorizzato pigramente come valore finale.

Come generatore, la funzione funzionerebbe (cioè stampando ogni valore a turno), ma quando si trasforma in una lista scorre sul generatore , quindi tutte le chiamate a cage( cage.animal) restituiscono gatti.


0

Semplifichiamo la domanda. Definire:

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

        yield (animal, pet_function)

Quindi, proprio come nella domanda, otteniamo:

>>> 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.

Ma se evitiamo di creare un list()primo:

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

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

Cosa sta succedendo? Perché questa sottile differenza cambia completamente i nostri risultati?


Se guardiamo list(get_petters()), è chiaro dal cambiamento degli indirizzi di memoria che effettivamente produciamo tre diverse funzioni:

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

Tuttavia, dai un'occhiata ai messaggi a cellcui sono associate queste funzioni:

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

Per entrambi i cicli, l' celloggetto rimane lo stesso durante le iterazioni. Tuttavia, come previsto, la specifica a cui strfa riferimento varia nel secondo ciclo. L' celloggetto si riferisce a animal, che viene creato quando get_petters()viene chiamato. Tuttavia, animalcambia l' stroggetto a cui si riferisce durante l'esecuzione della funzione generatore .

Nel primo ciclo, durante ogni iterazione, creiamo tutte le fs, ma le chiamiamo solo dopo che il generatore get_petters()è completamente esaurito e una listdelle funzioni è già stata creata.

Nel secondo ciclo, durante ogni iterazione, mettiamo in pausa il get_petters()generatore e chiamiamo fdopo ogni pausa. Quindi, finiamo per recuperare il valore di animalin quel momento nel tempo in cui la funzione del generatore è in pausa.

Come @Claudiu risponde a una domanda simile :

Vengono create tre funzioni separate, ma ciascuna ha la chiusura dell'ambiente in cui sono definite: in questo caso, l'ambiente globale (o l'ambiente della funzione esterna se il ciclo è posto all'interno di un'altra funzione). Questo è esattamente il problema, però: in questo ambiente, animalè mutato e le chiusure si riferiscono tutte allo stesso animal.

[Nota dell'editore: iè stata modificata in animal.]

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.