Was erfassen (Lambda) -Funktionsverschlüsse?

249

Vor kurzem habe ich angefangen, mit Python herumzuspielen, und bin auf etwas Besonderes gestoßen, wie Verschlüsse funktionieren. Betrachten Sie den folgenden Code:

adders=[0,1,2,3]

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

print adders[1](3)

Es wird ein einfaches Array von Funktionen erstellt, die eine einzelne Eingabe verwenden und die durch eine Zahl hinzugefügte Eingabe zurückgeben. Die Funktionen sind in einer forSchleife aufgebaut, in der der Iterator ivon 0bis ausgeführt wird 3. Für jede dieser Zahlen wird eine lambdaFunktion erstellt, die sie erfasst iund zur Eingabe der Funktion hinzufügt. Die letzte Zeile ruft die zweite lambdaFunktion mit 3als Parameter auf. Zu meiner Überraschung war die Ausgabe 6.

Ich habe a erwartet 4. Meine Argumentation war: In Python ist alles ein Objekt und daher ist jede Variable ein wesentlicher Zeiger darauf. Beim Erstellen der lambdaClosures für habe iich erwartet, dass ein Zeiger auf das Integer-Objekt gespeichert wird, auf das aktuell verwiesen wird i. Das bedeutet, dass beim iZuweisen eines neuen Ganzzahlobjekts die zuvor erstellten Abschlüsse nicht beeinflusst werden sollten. Leider zeigt die Überprüfung des addersArrays in einem Debugger, dass dies der Fall ist. Alle lambdaFunktionen beziehen sich auf den letzten Wert i, zeigt 3, deren Ergebnisse in der adders[1](3)Rückkehr 6.

Was mich über Folgendes wundern lässt:

  • Was erfassen die Verschlüsse genau?
  • Was ist die eleganteste Methode, um die lambdaFunktionen davon zu überzeugen , den aktuellen Wert von iauf eine Weise zu erfassen , die nicht beeinflusst wird, wenn isich ihr Wert ändert?
Boas
quelle
35
Ich habe dieses Problem im UI-Code gehabt. Hat mich verrückt gemacht. Der Trick besteht darin, sich daran zu erinnern, dass Schleifen keinen neuen Bereich erstellen.
Detly
3
@ TimMB Wie iverlässt man den Namespace?
Detly
3
@detly Nun, ich wollte sagen, dass print idas nach der Schleife nicht funktionieren würde. Aber ich habe es selbst getestet und jetzt verstehe ich, was du meinst - es funktioniert. Ich hatte keine Ahnung, dass Schleifenvariablen nach dem Schleifenkörper in Python verweilten.
Tim MB
1
@ TimMB - Ja, das habe ich gemeint. Das Gleiche gilt für if, with, tryusw.
detly
13
Dies finden Sie in den offiziellen Python-FAQ unter Warum geben Lambdas, die in einer Schleife mit unterschiedlichen Werten definiert sind, alle das gleiche Ergebnis zurück? , mit einer Erklärung und der üblichen Problemumgehung.
Abarnert

Antworten:

161

Ihre zweite Frage wurde beantwortet, aber Ihre erste:

Was erfasst der Verschluss genau?

Das Scoping in Python ist dynamisch und lexikalisch. Ein Abschluss merkt sich immer den Namen und den Umfang der Variablen, nicht das Objekt, auf das sie zeigt. Da alle Funktionen in Ihrem Beispiel im selben Bereich erstellt wurden und denselben Variablennamen verwenden, beziehen sie sich immer auf dieselbe Variable.

EDIT: In Bezug auf Ihre andere Frage, wie Sie dies überwinden können, gibt es zwei Möglichkeiten, die Ihnen in den Sinn kommen:

  1. Der prägnanteste, aber nicht genau gleichwertige Weg ist der 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 beibehalten werden soll.

  2. Etwas ausführlicher, aber weniger hackig 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 hier wird mithilfe einer neuen Funktion (der Kürze halber ein Lambda) erstellt, die das Argument bindet und den Wert, den Sie binden möchten, als Argument übergibt. In echtem Code haben Sie jedoch höchstwahrscheinlich eine normale Funktion anstelle des Lambda, um den neuen Bereich zu erstellen:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
    
Max Shawabkeh
quelle
1
Max, wenn Sie eine Antwort für meine andere (einfachere Frage) hinzufügen, kann ich dies als akzeptierte Antwort markieren. Vielen Dank!
Boaz
3
Python hat statisches Scoping, kein dynamisches Scoping. Es sind nur alle Variablen Referenzen. Wenn Sie also eine Variable auf ein neues Objekt setzen, hat die Variable selbst (die Referenz) dieselbe Position, zeigt jedoch auf etwas anderes. Das gleiche passiert in Schema, wenn Sie set!. Hier erfahren Sie, was dynamischer Bereich wirklich ist: voidspace.org.uk/python/articles/code_blocks.shtml .
Claudiu
6
Option 2 ähnelt den funktionalen Sprachen, die als "Curried-Funktion" bezeichnet werden.
Crashworks
205

Sie können die Erfassung einer Variablen mithilfe eines Arguments 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

Die Idee ist, einen Parameter (geschickt benannt i) zu deklarieren und ihm einen Standardwert der Variablen zu geben, die Sie erfassen möchten (den Wert von i).

Adrien Plisson
quelle
7
+1 für die Verwendung von Standardwerten. Die Bewertung bei der Definition des Lambda macht sie perfekt für diese Verwendung.
Quornian
21
+1 auch, weil dies die Lösung ist, die von den offiziellen FAQ empfohlen wird .
Abarnert
23
Das ist großartig. Das Standardverhalten von Python ist jedoch nicht.
Cecil Curry
1
Dies scheint jedoch keine gute Lösung zu sein ... Sie ändern tatsächlich die Funktionssignatur, nur um eine Kopie der Variablen zu erfassen. Und auch diejenigen, die die Funktion aufrufen, können mit der i-Variablen herumspielen, oder?
David Callanan
@DavidCallanan Wir sprechen von einem Lambda: einer Art Ad-hoc-Funktion, die Sie normalerweise in Ihrem eigenen Code definieren, um ein Loch zu schließen, und nicht etwas, das Sie durch ein ganzes SDK teilen. Wenn Sie eine stärkere Signatur benötigen, sollten Sie eine echte Funktion verwenden.
Adrien Plisson
33

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

Beim Importieren von Add from Operator, wie von Chris Lutz vorgeschlagen, wird das Beispiel wie folgt:

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)
Joma
quelle
24

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 denken 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. Es ist die Schleife? Oder vielleicht das Lambda?

Immerhin 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
Truppo
quelle
11
Es ist die Schleife, weil in vielen anderen Sprachen eine Schleife einen neuen Bereich erstellen kann.
Detly
1
Diese Antwort ist gut, weil sie erklärt, warum ifür jede Lambda-Funktion auf dieselbe Variable zugegriffen wird.
David Callanan
3

Als Antwort auf Ihre zweite Frage besteht die eleganteste Möglichkeit darin, eine Funktion zu verwenden, die zwei Parameter anstelle eines Arrays verwendet:

add = lambda a, b: a + b
add(1, 3)

Allerdings ist die Verwendung von Lambda hier etwas albern. Python gibt uns das operatorModul, das eine funktionale Schnittstelle zu den Basisoperatoren bietet. Das obige Lambda hat unnötigen Overhead, nur um den Additionsoperator aufzurufen:

from operator import add
add(1, 3)

Ich verstehe, dass Sie herumspielen und versuchen, die Sprache zu erkunden, aber ich kann mir keine Situation vorstellen, in der ich eine Reihe von Funktionen verwenden würde, bei denen Pythons Scoping-Verrücktheit im Weg stehen würde.

Wenn Sie möchten, können Sie eine kleine Klasse schreiben, die Ihre Array-Indizierungssyntax verwendet:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)
Chris Lutz
quelle
2
Chris, natürlich hat der obige Code nichts mit meinem ursprünglichen Problem zu tun. Es ist so konstruiert, dass es meinen Standpunkt auf einfache Weise veranschaulicht. Es ist natürlich sinnlos und albern.
Boaz
3

Hier ist ein neues Beispiel, das die Datenstruktur und den Inhalt eines Abschlusses 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 in einer Schließung?

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

Insbesondere befindet sich my_str nicht im Abschluss von f1.

Was ist in der Schließung von f2?

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

Beachten Sie (aus den Speicheradressen), dass beide Verschlüsse dieselben Objekte enthalten. Sie können sich also vorstellen , dass die Lambda-Funktion einen Verweis auf den Bereich enthält. My_str befindet sich jedoch nicht im Abschluss für f_1 oder f_2, und i befindet sich nicht im Abschluss für f_3 (nicht gezeigt), was darauf hindeutet, dass die Abschlussobjekte selbst unterschiedliche Objekte sind.

Sind die Verschlussobjekte selbst dasselbe Objekt?

>>> print f_1.func_closure is f_2.func_closure
False
Jeff
quelle
NB Die Ausgabe int object at [address X]>ließ mich denken, dass der Abschluss [Adresse X] AKA als Referenz speichert. [Adresse X] ändert sich jedoch, wenn die Variable nach der Lambda-Anweisung neu zugewiesen wird.
Jeff