Funktionen in einer Schleife erstellen

102

Ich versuche, Funktionen innerhalb einer Schleife zu erstellen:

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

Das Problem ist, dass alle Funktionen gleich sind. Anstatt 0, 1 und 2 zurückzugeben, geben alle drei Funktionen 2 zurück:

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

Warum passiert das und was soll ich tun, um 3 verschiedene Funktionen zu erhalten, die jeweils 0, 1 und 2 ausgeben?

Sharvey
quelle
4
als Erinnerung an mich selbst: docs.python-guide.org/en/latest/writing/gotchas/…
Chuntao Lu

Antworten:

167

Sie haben ein Problem mit der späten Bindung - jede Funktion wird iso spät wie möglich angezeigt (wenn sie nach dem Ende der Schleife aufgerufen wird, iwird sie auf gesetzt 2).

Einfach durch Erzwingen einer frühen Bindung def f():zu beheben: Ändern Sie dies def f(i=i):wie folgt:

def f(i=i):
    return i

Die Standardwerte (die rechte iin i=iein Standardwert für Argumentnamen i, die die linke ist iin i=i) an nachgeschlagen defZeit, nicht an der callZeit, so im Wesentlichen sind sie einen Weg , um gezielt nach frühe Bindung.

Wenn Sie sich Sorgen machen f, ein zusätzliches Argument zu erhalten (und daher möglicherweise fälschlicherweise aufgerufen zu werden), gibt es eine ausgefeiltere Methode, bei der ein Verschluss als "Funktionsfactory" verwendet wird:

def make_f(i):
    def f():
        return i
    return f

und in Ihrer Schleife f = make_f(i)anstelle der defAnweisung verwenden.

Alex Martelli
quelle
7
Woher wissen Sie, wie Sie diese Probleme beheben können?
Alwbtc
3
@alwbtc es ist meistens nur Erfahrung, die meisten Leute haben sich diesen Dingen irgendwann selbst gestellt.
Ruohola
Können Sie bitte erklären, warum es funktioniert? (Sie speichern mich bei einem Rückruf, der in einer Schleife generiert wurde. Argumente waren immer die letzten in der Schleife. Vielen Dank!)
Vincent Bénet
20

Die Erklärung

Das Problem hierbei ist, dass der Wert von ibeim Erstellen der Funktion nicht gespeichert fwird. Schlägt vielmehr fden Wert des iAufrufs nach .

Wenn Sie darüber nachdenken, ist dieses Verhalten absolut sinnvoll. Tatsächlich ist dies die einzig vernünftige Art und Weise, wie Funktionen funktionieren können. Stellen Sie sich vor, Sie haben eine Funktion, die auf eine globale Variable zugreift, wie folgt:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Wenn Sie diesen Code lesen, würden Sie natürlich erwarten, dass er "bar" und nicht "foo" druckt, da sich der Wert von global_varnach der Deklaration der Funktion geändert hat. Dasselbe passiert in Ihrem eigenen Code: Zum Zeitpunkt Ihres Aufrufs fhat sich der Wert von igeändert und wurde auf gesetzt 2.

Die Lösung

Es gibt tatsächlich viele Möglichkeiten, dieses Problem zu lösen. Hier sind einige Optionen:

  • Erzwingen Sie eine frühzeitige Bindung, iindem Sie sie als Standardargument verwenden

    Im Gegensatz zu Abschlussvariablen (wie i) werden Standardargumente sofort ausgewertet, wenn die Funktion definiert wird:

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)
    

    Um einen kleinen Einblick zu geben, wie / warum dies funktioniert: Die Standardargumente einer Funktion werden als Attribut der Funktion gespeichert. Somit wird der aktuelle Wert von aufgenommen iund gespeichert.

    >>> i = 0
    >>> def f(i=i):
    ...     pass
    >>> f.__defaults__  # this is where the current value of i is stored
    (0,)
    >>> # assigning a new value to i has no effect on the function's default arguments
    >>> i = 5
    >>> f.__defaults__
    (0,)
    
  • Verwenden Sie eine Funktionsfactory, um den aktuellen Wert ieines Verschlusses zu erfassen

    Die Wurzel Ihres Problems ist, dass isich eine Variable ändern kann. Wir können dieses Problem umgehen, indem wir eine weitere Variable erstellen , die sich garantiert nie ändert - und der einfachste Weg, dies zu tun, ist ein Abschluss :

    def f_factory(i):
        def f():
            return i  # i is now a *local* variable of f_factory and can't ever change
        return f
    
    for i in range(3):           
        f = f_factory(i)
        functions.append(f)
    
  • Verwenden Sie functools.partialdiese Option , um den aktuellen Wert von ian zu bindenf

    functools.partialMit dieser Option können Sie einer vorhandenen Funktion Argumente hinzufügen. In gewisser Weise ist es auch eine Art Funktionsfabrik.

    import functools
    
    def f(i):
        return i
    
    for i in range(3):    
        f_with_i = functools.partial(f, i)  # important: use a different variable than "f"
        functions.append(f_with_i)
    

Vorsichtsmaßnahme: Diese Lösungen funktionieren nur, wenn Sie der Variablen einen neuen Wert zuweisen . Wenn Sie das in der Variablen gespeicherte Objekt ändern , tritt erneut dasselbe Problem auf:

>>> i = []  # instead of an int, i is now a *mutable* object
>>> def f(i=i):
...     print('i =', i)
...
>>> i.append(5)  # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]

Beachten Sie, wie sich inoch geändert hat, obwohl wir daraus ein Standardargument gemacht haben! Wenn Ihr Code mutiert i , müssen Sie eine Kopie davon iwie folgt an Ihre Funktion binden :

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
Aran-Fey
quelle