Wie funktionieren lexikalische Verschlüsse?

149

Während ich ein Problem untersuchte, das ich mit lexikalischen Abschlüssen in Javascript-Code hatte, stieß ich in Python auf dieses Problem:

flist = []

for i in xrange(3):
    def func(x): return x * i
    flist.append(func)

for f in flist:
    print f(2)

Beachten Sie, dass dieses Beispiel achtsam vermeidet lambda. Es druckt "4 4 4", was überraschend ist. Ich würde "0 2 4" erwarten.

Dieser äquivalente Perl-Code macht es richtig:

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * $_[0]});
}

foreach my $f (@flist)
{
    print $f->(2), "\n";
}

"0 2 4" wird gedruckt.

Können Sie bitte den Unterschied erklären?


Aktualisieren:

Das Problem ist nicht mit iglobal sein. Dies zeigt das gleiche Verhalten:

flist = []

def outer():
    for i in xrange(3):
        def inner(x): return x * i
        flist.append(inner)

outer()
#~ print i   # commented because it causes an error

for f in flist:
    print f(2)

Wie die kommentierte Zeile zeigt, iist zu diesem Zeitpunkt unbekannt. Trotzdem wird "4 4 4" gedruckt.

Eli Bendersky
quelle
3
Hier ist ein ziemlich guter Artikel zu diesem Thema. me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

Antworten:

151

Python verhält sich tatsächlich wie definiert. 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 ist ich mutiert und die Verschlüsse beziehen sich alle auf dasselbe i .

Hier ist die beste Lösung, die ich finden kann - erstellen Sie einen Funktionsersteller und rufen Sie diesen stattdessen auf. Dies erzwingt unterschiedliche Umgebungen für jede der erstellten Funktionen mit jeweils einem anderen i .

flist = []

for i in xrange(3):
    def funcC(j):
        def func(x): return x * j
        return func
    flist.append(funcC(i))

for f in flist:
    print f(2)

Dies passiert, wenn Sie Nebenwirkungen und funktionale Programmierung mischen.

Claudiu
quelle
5
Ihre Lösung wird auch in Javascript verwendet.
Eli Bendersky
9
Dies ist kein Fehlverhalten. Es verhält sich genau wie definiert.
Alex Coventry
6
IMO Piro hat eine bessere Lösung stackoverflow.com/questions/233673/…
jfs
2
Ich würde vielleicht das innerste 'i' aus Gründen der Klarheit in 'j' ändern.
Eggsyntax
7
Was ist mit der Definition so:def inner(x, i=i): return x * i
Bindestrich
152

Die in der Schleife definierten Funktionen greifen weiterhin auf dieselbe Variable zu, iwährend sich ihr Wert ändert. Am Ende der Schleife zeigen alle Funktionen auf dieselbe Variable, die den letzten Wert in der Schleife enthält: Der Effekt ist der im Beispiel angegebene.

Um iseinen Wert auszuwerten und zu verwenden, wird er häufig als Parameterstandard festgelegt: Parameterstandards werden ausgewertet, wenn die defAnweisung ausgeführt wird, und daher wird der Wert der Schleifenvariablen eingefroren.

Folgendes funktioniert wie erwartet:

flist = []

for i in xrange(3):
    def func(x, i=i): # the *value* of i is copied in func() environment
        return x * i
    flist.append(func)

for f in flist:
    print f(2)
Piro
quelle
7
s / zur Kompilierungszeit / zu dem Zeitpunkt, zu dem die defAnweisung ausgeführt wird /
jfs
23
Dies ist eine geniale Lösung, die es schrecklich macht.
Stavros Korokithakis
Bei dieser Lösung gibt es ein Problem: func hat jetzt zwei Parameter. Das heißt, es funktioniert nicht mit einer variablen Anzahl von Parametern. Schlimmer noch, wenn Sie func mit einem zweiten Parameter aufrufen, wird das Original iaus der Definition überschrieben . :-(
Pascal
34

So machen Sie es mit der functoolsBibliothek (von der ich nicht sicher bin, ob sie zum Zeitpunkt der Fragestellung verfügbar war).

from functools import partial

flist = []

def func(i, x): return x * i

for i in xrange(3):
    flist.append(partial(func, i))

for f in flist:
    print f(2)

Gibt erwartungsgemäß 0 2 4 aus.

Luca Invernizzi
quelle
Ich wollte das unbedingt verwenden, aber meine Funktion ist eigentlich eine Klassenmethode und der erste übergebene Wert ist self. Gibt es das überhaupt?
Michael David Watson
1
Absolut. Angenommen, Sie haben eine Klasse Math mit einer Methode add (self, a, b) und möchten a = 1 setzen, um die 'Inkrement'-Methode zu erstellen. Erstellen Sie dann eine Instanz Ihrer Klasse 'my_math', und Ihre Inkrementierungsmethode lautet 'increment = partiell (my_math.add, 1)'.
Luca Invernizzi
2
Um diese Technik auf eine Methode anzuwenden, können Sie sie auch functools.partialmethod()ab Python 3.4
Matt Eding
13

Schau dir das an:

for f in flist:
    print f.func_closure


(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)

Dies bedeutet, dass alle auf dieselbe i-Variableninstanz verweisen, die nach Beendigung der Schleife den Wert 2 hat.

Eine lesbare Lösung:

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))
Null303
quelle
1
Meine Frage ist eher "allgemein". Warum hat Python diesen Fehler? Ich würde erwarten, dass eine Sprache, die lexikalische Abschlüsse unterstützt (wie Perl und die gesamte Lisp-Dynastie), dies richtig funktioniert.
Eli Bendersky
2
Die Frage, warum etwas einen Fehler aufweist, setzt voraus, dass es kein Fehler ist.
Null303
7

Was passiert ist, dass die Variable i erfasst wird und die Funktionen den Wert zurückgeben, an den sie zum Zeitpunkt des Aufrufs gebunden ist. In funktionalen Sprachen tritt diese Art von Situation nie auf, da ich nicht zurückprallen würde. Bei Python und auch bei Lisp ist dies jedoch nicht mehr der Fall.

Der Unterschied zu Ihrem Schema-Beispiel besteht in der Semantik der do-Schleife. Das Schema erstellt effektiv jedes Mal eine neue i-Variable durch die Schleife, anstatt eine vorhandene i-Bindung wie bei den anderen Sprachen wiederzuverwenden. Wenn Sie eine andere Variable verwenden, die außerhalb der Schleife erstellt und mutiert wurde, wird im Schema dasselbe Verhalten angezeigt. Versuchen Sie, Ihre Schleife durch Folgendes zu ersetzen:

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

Schauen Sie hier, um weitere Diskussionen darüber zu führen.

[Bearbeiten] Möglicherweise ist es besser, sich die do-Schleife als ein Makro vorzustellen, das die folgenden Schritte ausführt:

  1. Definieren Sie ein Lambda anhand eines einzelnen Parameters (i), wobei ein Körper durch den Körper der Schleife definiert wird.
  2. Ein sofortiger Aufruf dieses Lambda mit entsprechenden Werten von i als Parameter.

dh. das Äquivalent zu der folgenden Python:

flist = []

def loop_body(i):      # extract body of the for loop to function
    def func(x): return x*i
    flist.append(func)

map(loop_body, xrange(3))  # for i in xrange(3): body

Das i ist nicht mehr das aus dem übergeordneten Bereich, sondern eine brandneue Variable in ihrem eigenen Bereich (dh der Parameter für das Lambda), sodass Sie das beobachtete Verhalten erhalten. Python hat diesen impliziten neuen Bereich nicht, daher teilt der Hauptteil der for-Schleife nur die Variable i.

Brian
quelle
Interessant. Ich war mir des Unterschieds in der Semantik der do-Schleife nicht bewusst. Danke
Eli Bendersky
4

Ich bin immer noch nicht ganz davon überzeugt, warum dies in einigen Sprachen so und auf andere Weise funktioniert. In Common Lisp ist es wie in Python:

(defvar *flist* '())

(dotimes (i 3 t)
  (setf *flist* 
    (cons (lambda (x) (* x i)) *flist*)))

(dolist (f *flist*)  
  (format t "~a~%" (funcall f 2)))

Druckt "6 6 6" (beachten Sie, dass hier die Liste von 1 bis 3 ist und umgekehrt aufgebaut ist "). Während in Schema funktioniert es wie in Perl:

(define flist '())

(do ((i 1 (+ 1 i)))
    ((>= i 4))
  (set! flist 
    (cons (lambda (x) (* i x)) flist)))

(map 
  (lambda (f)
    (printf "~a~%" (f 2)))
  flist)

Druckt "6 4 2"

Und wie ich bereits erwähnt habe, befindet sich Javascript im Python / CL-Camp. Es scheint, dass es hier eine Implementierungsentscheidung gibt, die verschiedene Sprachen auf unterschiedliche Weise angehen. Ich würde gerne genau verstehen, was die Entscheidung ist.

Eli Bendersky
quelle
8
Der Unterschied liegt eher in den (do ...) als in den Scoping-Regeln. Im Schema erstellt do bei jedem Durchlauf durch die Schleife eine neue Variable, während andere Sprachen die vorhandene Bindung wiederverwenden. In meiner Antwort finden Sie weitere Details und ein Beispiel für eine Schemaversion mit einem ähnlichen Verhalten wie lisp / python.
Brian
2

Das Problem ist, dass alle lokalen Funktionen an dieselbe Umgebung und damit an dieselbe iVariable gebunden sind. Die Lösung (Problemumgehung) besteht darin, für jede Funktion (oder jedes Lambda) separate Umgebungen (Stapelrahmen) zu erstellen:

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]

>>> t[1](2)
2
>>> t[2](2)
4
Rafał Dowgird
quelle
1

Die Variable iist eine globale Variable , deren Wert bei jedem fAufruf der Funktion 2 beträgt.

Ich wäre geneigt, das Verhalten, nach dem Sie suchen, wie folgt umzusetzen:

>>> class f:
...  def __init__(self, multiplier): self.multiplier = multiplier
...  def __call__(self, multiplicand): return self.multiplier*multiplicand
... 
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]

Antwort auf Ihr Update : Es ist nicht die Globalität an i sich, die dieses Verhalten verursacht, sondern die Tatsache, dass es sich um eine Variable aus einem umschließenden Bereich handelt, die über die Zeiten, in denen f aufgerufen wird, einen festen Wert hat. In Ihrem zweiten Beispiel wird der Wert von iaus dem kkkFunktionsumfang übernommen, und daran ändert sich nichts, wenn Sie die Funktionen aufrufen flist.

Alex Coventry
quelle
0

Die Gründe für das Verhalten wurden bereits erklärt und es wurden mehrere Lösungen veröffentlicht, aber ich denke, dies ist die pythonischste (denken Sie daran, alles in Python ist ein Objekt!):

flist = []

for i in xrange(3):
    def func(x): return x * func.i
    func.i=i
    flist.append(func)

for f in flist:
    print f(2)

Claudius Antwort ist ziemlich gut, wenn man einen Funktionsgenerator verwendet, aber Piros Antwort ist ein Hack, um ehrlich zu sein, da es mich zu einem "versteckten" Argument mit einem Standardwert macht (es wird gut funktionieren, aber es ist nicht "pythonisch"). .

Darkfeline
quelle
Ich denke, es hängt von Ihrer Python-Version ab. Jetzt bin ich erfahrener und würde diese Vorgehensweise nicht mehr vorschlagen. Claudius ist der richtige Weg, um in Python einen Abschluss zu machen.
Darkfeline
1
Dies funktioniert weder mit Python 2 noch mit Python 3 (beide geben "4 4 4" aus). Das funcIn x * func.ibezieht sich immer auf die zuletzt definierte Funktion. Obwohl jede Funktion einzeln die richtige Nummer hat, lesen sie trotzdem alle von der letzten.
Lambda Fairy