Die Bindung von Python Lambda an lokale Werte

74

Der folgende Code spuckt 1zweimal aus, aber ich erwarte zu sehen 0und dann 1.

def pv(v) :
  print v

x = []
for v in range(2):
  x.append(lambda : pv(v))

for xx in x:
  xx()

Ich hatte erwartet, dass Python-Lambdas hinter den Kulissen an die Referenz binden, auf die eine lokale Variable zeigt. Dies scheint jedoch nicht der Fall zu sein. Ich bin auf dieses Problem in einem großen System gestoßen, in dem das Lambda das moderne C ++ - Äquivalent einer Bindung ausführt (z. B. 'boost :: bind'), in dem Sie in einem solchen Fall an ein intelligentes ptr binden oder eine Kopie erstellen würden, um eine Kopie für das Lambda zu erstellen.

Wie binde ich eine lokale Variable an eine Lambda-Funktion und lasse sie bei Verwendung die richtige Referenz beibehalten? Ich bin ziemlich begeistert von dem Verhalten, da ich dies von einer Sprache mit einem Müllsammler nicht erwarten würde.

Hassan Syed
quelle
2
Ich habe mehrere Varianten dieser Frage gesehen. jemand muss sie alle zusammenfassen, den Titel in etwas Denkwürdiges ändern und dann dem Internet sagen, dass er das googeln soll
Shep
2
Ah, los geht's, fast doppelt: lexikalische Verschlüsse in Python
Shep
@shep, ja, ich konnte es nicht richtig formulieren, ich habe versucht, die Funktionsweise von lua zu replizieren.
Hassan Syed
2
Der seltsame Teil von Python ist, dass Schleifenvariablen nach der Schleife bestehen bleiben, sodass Funktionen, die innerhalb der Schleife definiert sind und auf die Schleifenvariable verweisen, kein NameErrorif auslösen, wenn sie außerhalb der Schleife aufgerufen werden. In den meisten anderen Sprachen sind Schleifenvariablen nur innerhalb der Schleife selbst zugänglich.
BallpointBen

Antworten:

116

Wechseln Sie x.append(lambda : pv(v))zu x.append(lambda v=v: pv(v)).

Sie erwarten, dass "Python-Lambdas an die Referenz binden, auf die eine lokale Variable hinter den Kulissen zeigt", aber so funktioniert Python nicht. Python sucht den Variablennamen zum Zeitpunkt des Aufrufs der Funktion und nicht zum Zeitpunkt der Erstellung. Die Verwendung eines Standardarguments funktioniert, da Standardargumente beim Erstellen der Funktion ausgewertet werden und nicht beim Aufrufen.

Das ist nichts Besonderes an Lambdas. Erwägen:

x = "before foo defined"
def foo():
    print x
x = "after foo was defined"
foo()

druckt

after foo was defined
Steven Rumbalski
quelle
Interessant, könnten Sie bitte die Intuition der Mechanik näher erläutern.
Hassan Syed
5
Eine naheliegendere Alternative ist die Verwendung functools.partialoder Verwendung einer separaten Hilfsfunktion zum Erstellen der Verschlüsse ( def make_closure(v): return lambda: pv(v)in die Sie sie einfügen können test).
Also sollte mein richtiger Code gefallen lambda par1, par2 = l3_e : self.parse_l4(par1, par2)?
Hassan Syed
Wenn vselbst eine Referenz war (im Gegensatz zu einer int), dann nehme ich an, dass es keine "tiefe" Kopie geben wird?
Aaron McDaid
27

Der Abschluss des Lambda enthält einen Verweis auf die verwendete Variable und nicht auf ihren Wert. Wenn sich also der Wert der Variablen später ändert, ändert sich auch der Wert im Abschluss. Das heißt, der Wert der Abschlussvariablen wird beim Aufruf der Funktion aufgelöst, nicht beim Erstellen. (Pythons Verhalten hier ist in der Welt der funktionalen Programmierung nicht ungewöhnlich, was es wert ist.)

Es gibt zwei Lösungen:

  1. Verwenden Sie ein Standardargument, das den aktuellen Wert der Variablen zum Zeitpunkt der Definition an einen lokalen Namen bindet. lambda v=v: pv(v)

  2. Verwenden Sie ein Doppel-Lambda und rufen Sie sofort das erste an. (lambda v: lambda: pv(v))(v)

irgendwie
quelle