Lokale Variablen in verschachtelten Funktionen

105

Okay, nehmen Sie das mit, ich weiß, es wird schrecklich verworren aussehen, aber bitte helfen Sie mir zu verstehen, was passiert.

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

Gibt:

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

Warum bekomme ich nicht drei verschiedene Tiere? Ist das nicht cagein den lokalen Bereich der verschachtelten Funktion 'gepackt'? Wenn nicht, wie sucht ein Aufruf der verschachtelten Funktion die lokalen Variablen?

Ich weiß, dass solche Probleme normalerweise bedeuten, dass man es falsch macht, aber ich würde gerne verstehen, was passiert.

noio
quelle
1
Versuchen Sie for animal in ['cat', 'dog', 'cow']... Ich bin sicher, jemand wird mitkommen und dies erklären - es ist eines dieser Python-Fallstricke :)
Jon Clements

Antworten:

114

Die verschachtelte Funktion sucht bei der Ausführung nach Variablen aus dem übergeordneten Bereich, nicht nach der Definition.

Der Funktionskörper wird kompiliert und die 'freien' Variablen (nicht in der Funktion selbst durch Zuweisung definiert) werden überprüft und dann als Abschlusszellen an die Funktion gebunden, wobei der Code einen Index verwendet, um auf jede Zelle zu verweisen. pet_functionhat also eine freie Variable ( cage), auf die dann über eine Abschlusszelle, Index 0, verwiesen wird. Der Abschluss selbst zeigt auf die lokale Variable cagein der get_pettersFunktion.

Wenn Sie die Funktion tatsächlich aufrufen, wird dieser Abschluss verwendet, um den Wert von cageim umgebenden Bereich zum Zeitpunkt des Aufrufs der Funktion anzuzeigen . Hier liegt das Problem. Wenn Sie Ihre Funktionen aufrufen, get_pettersist die Berechnung der Ergebnisse bereits abgeschlossen. Die cagelokale Variable an einem gewissen Punkt während dieser Ausführung wurde jede der zugeordneten 'cow', 'dog'und 'cat'Zeichenfolge, aber am Ende der Funktion cageenthält, die letzten Wert 'cat'. Wenn Sie also jede der dynamisch zurückgegebenen Funktionen aufrufen, wird der Wert 'cat'gedruckt.

Die Problemumgehung besteht darin, sich nicht auf Verschlüsse zu verlassen. Sie können stattdessen eine Teilfunktion verwenden, einen neuen Funktionsbereich erstellen oder die Variable als Standardwert für einen Schlüsselwortparameter binden .

  • Teilfunktionsbeispiel mit 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)))
  • Erstellen eines neuen Bereichsbeispiels:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Binden der Variablen als Standardwert für einen Schlüsselwortparameter:

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

Die scoped_cageFunktion in der Schleife muss nicht definiert werden. Die Kompilierung erfolgt nur einmal und nicht bei jeder Iteration der Schleife.

Martijn Pieters
quelle
1
Ich habe heute 3 Stunden lang meinen Kopf an diese Wand geschlagen, um ein Skript für die Arbeit zu erstellen. Ihr letzter Punkt ist sehr wichtig und der Hauptgrund, warum ich auf dieses Problem gestoßen bin. Ich habe Rückrufe mit unzähligen Verschlüssen in meinem Code, aber die gleiche Technik in einer Schleife zu versuchen, hat mich dazu gebracht.
DrEsperanto
12

Nach meinem Verständnis wird im Namespace der übergeordneten Funktion nach Käfig gesucht, wenn die angegebene pet_function tatsächlich aufgerufen wird, nicht vorher.

Also wenn du es tust

funs = list(get_petters())

Sie generieren 3 Funktionen, die den zuletzt erstellten Käfig finden.

Wenn Sie Ihre letzte Schleife ersetzen durch:

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

Sie erhalten tatsächlich:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Nicolas Barbey
quelle
6

Dies ergibt sich aus dem Folgenden

for i in range(2): 
    pass

print(i)  # prints 1

Nach der Iteration wird der Wert von iträge als Endwert gespeichert.

Als Generator würde die Funktion funktionieren (dh jeden Wert nacheinander drucken), aber wenn sie in eine Liste umgewandelt wird, läuft sie über den Generator , daher werden alle Aufrufe von cage( cage.animal) Katzen zurückgeben.

Andy Hayden
quelle
0

Vereinfachen wir die Frage. Definieren:

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

        yield (animal, pet_function)

Dann bekommen wir genau wie in der Frage:

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

Aber wenn wir es vermeiden, eine list()erste zu erstellen :

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

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

Was ist los? Warum verändert dieser subtile Unterschied unsere Ergebnisse vollständig?


Wenn wir uns das ansehen list(get_petters()), wird aus den sich ändernden Speicheradressen deutlich, dass wir tatsächlich drei verschiedene Funktionen liefern:

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

Schauen Sie sich jedoch die cells an, an die diese Funktionen gebunden sind:

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

Für beide Schleifen cellbleibt das Objekt während der Iterationen gleich. Wie erwartet strvariiert die spezifische Referenz in der zweiten Schleife. Das cellObjekt bezieht sich auf animaldas Objekt , das beim get_petters()Aufruf erstellt wird. Jedoch animaländert sich, was strObjekt , das es sich bezieht , wie die Generatorfunktion läuft .

In der ersten Schleife erstellen wir während jeder Iteration alle fs, rufen sie jedoch erst auf, wenn der Generator get_petters()vollständig erschöpft ist und bereits eine listder Funktionen erstellt wurde.

In der zweiten Schleife halten wir während jeder Iteration den get_petters()Generator an und rufen fnach jeder Pause auf. Auf diese Weise erhalten wir den Wert animalzu dem Zeitpunkt, zu dem die Generatorfunktion angehalten wurde.

Wie @Claudiu eine Antwort auf eine ähnliche Frage gibt :

Es werden drei separate Funktionen erstellt, die jedoch jeweils die Umgebung schließen, in der sie definiert sind - in diesem Fall die globale Umgebung (oder die Umgebung der äußeren Funktion, wenn die Schleife in einer anderen Funktion platziert ist). Dies ist jedoch genau das Problem - in dieser Umgebung animalist es mutiert und die Verschlüsse beziehen sich alle auf dasselbe animal.

[Anmerkung des Herausgebers: iwurde geändert in animal.]

Mateen Ulhaq
quelle