369 Stimmen

Was erfassen Lambda-Funktionsabschlüsse?

Kürzlich habe ich angefangen, mit Python herumzuspielen und bin dabei auf etwas Seltsames in der Funktionsweise von Closures gestoßen. Betrachten Sie den folgenden Code:

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

Es bildet eine einfache Reihe von Funktionen, die eine einzelne Eingabe annehmen und diese um eine Zahl erhöht zurückgeben. Die Funktionen werden konstruiert in for Schleife, in der der Iterator i läuft von 0 a 3 . Für jede dieser Zahlen a lambda wird eine Funktion erstellt, die Folgendes erfasst i und fügt es der Eingabe der Funktion hinzu. Die letzte Zeile ruft die zweite lambda Funktion mit 3 als Parameter. Zu meiner Überraschung war die Ausgabe 6 .

Ich hatte eine 4 . Meine Überlegung war: In Python ist alles ein Objekt und somit ist jede Variable im Wesentlichen ein Zeiger auf dieses Objekt. Beim Erstellen des lambda Verschlüsse für i erwartet, dass er einen Zeiger auf das Integer-Objekt speichert, auf das derzeit durch i . Das bedeutet, dass, wenn i ein neues Integer-Objekt zugewiesen wird, sollte dies keine Auswirkungen auf die zuvor erstellten Verschlüsse haben. Leider ist die Inspektion der adders Array in einem Debugger zeigt, dass dies der Fall ist. Alle lambda Funktionen beziehen sich auf den letzten Wert von i , 3 was zu folgenden Ergebnissen führt adders[1](3) zurückkehrend 6 .

Das wirft bei mir folgende Fragen auf:

  • Was genau wird mit den Verschlüssen erfasst?
  • Was ist der eleganteste Weg, um die lambda Funktionen zur Erfassung des aktuellen Wertes von i in einer Weise, die nicht beeinträchtigt wird, wenn i seinen Wert ändert?

318voto

Adrien Plisson Punkte 20874

Können Sie die Erfassung einer Variablen durch ein Argument mit einem Standardwert erzwingen:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

besteht die Idee darin, einen Parameter zu deklarieren (clevererweise mit i ) und geben Sie ihm einen Standardwert für die zu erfassende Variable (den Wert von i )

226voto

Max Shawabkeh Punkte 36359

Ihre zweite Frage wurde bereits beantwortet, aber was Ihre erste Frage betrifft:

Was genau wird mit dem Verschluss erfasst?

Scoping in Python ist dynamisch und lexikalisch. Eine Closure merkt sich immer den Namen und den Bereich der Variablen, nicht das Objekt, auf das sie zeigt. Da alle Funktionen in Ihrem Beispiel im gleichen Bereich erstellt werden und den gleichen Variablennamen verwenden, beziehen sie sich immer auf die gleiche Variable.

In Bezug auf Ihre andere Frage, wie man dieses Problem lösen kann, fallen mir zwei Möglichkeiten ein:

  1. Die prägnanteste, aber nicht unbedingt gleichwertige Methode ist die eine von Adrien Plisson empfohlene . Erstellen Sie ein Lambda mit einem zusätzlichen Argument und setzen Sie den Standardwert des zusätzlichen Arguments auf das Objekt, das Sie erhalten möchten.

  2. Eine etwas ausführlichere, aber weniger umständliche Lösung wäre es, jedes Mal, wenn Sie das Lambda erstellen, einen neuen Bereich zu erstellen:

     >>> adders = [0,1,2,3]
     >>> for i in [0,1,2,3]:
     ...     adders[i] = (lambda b: lambda a: b + a)(i)
     ...     
     >>> adders[1](3)
     4
     >>> adders[2](3)
     5

Der Bereich wird hier mit einer neuen Funktion (der Kürze halber ein Lambda) erstellt, die ihr Argument bindet und den Wert, den Sie binden wollen, als Argument übergibt. In echtem Code werden Sie jedoch höchstwahrscheinlich eine gewöhnliche Funktion anstelle des Lambdas verwenden, um den neuen Bereich zu erstellen:

def createAdder(x):
    return lambda y: y + x
adders = [createAdder(i) for i in range(4)]

52voto

Joma Punkte 2222

Der Vollständigkeit halber noch eine Antwort auf Ihre zweite Frage: Sie könnten verwenden teilweise im functools Modul.

Mit dem Import von add from operator, wie Chris Lutz vorgeschlagen hat, wird das Beispiel zu einem:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
    # store callable object with first argument given as (current) i
    adders[i] = partial(add, i) 

print adders[1](3)

33voto

mthurlin Punkte 24849

Betrachten Sie den folgenden Code:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Ich denke, die meisten Leute werden das überhaupt nicht verwirrend finden. Es ist das erwartete Verhalten.

Warum also glauben die Leute, dass es anders wäre, wenn es in einer Schleife gemacht wird? Ich weiß, dass ich diesen Fehler selbst gemacht habe, aber ich weiß nicht, warum. Liegt es an der Schleife? Oder vielleicht das Lambda?

Schließlich ist die Schleife nur eine kürzere Version von:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

6voto

Jeff Punkte 512

Hier ist ein neues Beispiel, das die Datenstruktur und den Inhalt einer Schließung hervorhebt, um zu verdeutlichen, wann der umschließende Kontext "gespeichert" wird.

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

Was ist ein Verschluss?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

Vor allem ist my_str nicht in der Schließung von f1 enthalten.

Was ist im Verschluss von f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Beachten Sie (anhand der Speicheradressen), dass beide Abschlüsse die gleichen Objekte enthalten. Sie können also Start zu denken, dass die Lambda-Funktion einen Verweis auf den Bereich hat. Allerdings ist my_str nicht in der Closure für f_1 oder f_2, und i ist nicht in der Closure für f_3 (nicht gezeigt), was darauf schließen lässt, dass die Closure-Objekte selbst unterschiedliche Objekte sind.

Sind die Abschlussobjekte selbst ein und dasselbe Objekt?

>>> print f_1.func_closure is f_2.func_closure
False

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X