Umfang der Lambda-Funktionen und ihrer Parameter?

87

Ich benötige eine Rückruffunktion, die für eine Reihe von GUI-Ereignissen fast identisch ist. Die Funktion verhält sich etwas anders, je nachdem, welches Ereignis sie aufgerufen hat. Scheint mir ein einfacher Fall zu sein, aber ich kann dieses seltsame Verhalten von Lambda-Funktionen nicht herausfinden.

Ich habe also den folgenden vereinfachten Code unten:

def callback(msg):
    print msg

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(m))
for f in funcList:
    f()

#create one at a time
funcList=[]
funcList.append(lambda: callback('do'))
funcList.append(lambda: callback('re'))
funcList.append(lambda: callback('mi'))
for f in funcList:
    f()

Die Ausgabe dieses Codes lautet:

mi
mi
mi
do
re
mi

Ich erwartete:

do
re
mi
do
re
mi

Warum hat die Verwendung eines Iterators die Dinge durcheinander gebracht?

Ich habe versucht, eine Deepcopy zu verwenden:

import copy
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
    f()

Das hat aber das gleiche Problem.

Agartland
quelle
3
Ihr Fragentitel ist etwas irreführend.
lispmachine
1
Warum Lambdas verwenden, wenn Sie sie verwirrend finden? Warum nicht def verwenden, um Funktionen zu definieren? Was macht Lambdas an Ihrem Problem so wichtig?
S.Lott
@ S.Lott Verschachtelte Funktion führt zu demselben Problem (möglicherweise deutlicher sichtbar)
lispmachine
1
@agartland: Bist du ich? Ich war zu der Arbeit an GUI - Ereignissen, und ich schrieb den folgenden fast identischen Test , bevor Sie diese Seite während der Hintergrundforschung zu finden: pastebin.com/M5jjHjFT
imallett
4
Siehe Warum geben Lambdas, die in einer Schleife mit unterschiedlichen Werten definiert sind, alle das gleiche Ergebnis zurück? in den offiziellen Programmier-FAQ für Python. Es erklärt das Problem ziemlich gut und bietet eine Lösung.
abarnert

Antworten:

78

Das Problem hierbei ist, dass die mVariable (eine Referenz) aus dem umgebenden Bereich entnommen wird. Im Lambda-Bereich werden nur Parameter gespeichert.

Um dies zu lösen, müssen Sie einen anderen Bereich für Lambda erstellen:

def callback(msg):
    print msg

def callback_factory(m):
    return lambda: callback(m)

funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(callback_factory(m))
for f in funcList:
    f()

Im obigen Beispiel verwendet Lambda auch den umgebenden Bereich, um zu suchen m, aber dieses Mal wird dieser callback_factoryBereich einmal pro callback_factory Aufruf erstellt.

Oder mit functools.partial :

from functools import partial

def callback(msg):
    print msg

funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
    f()
lispmachine
quelle
2
Diese Erklärung ist etwas irreführend. Das Problem ist die Änderung des Wertes von m in der Iteration, nicht der Bereich.
Ixx
Der obige Kommentar ist wahr, wie von @abarnert im Kommentar zu der Frage angegeben, wo auch ein Link angegeben ist, der das Phenonimon und die Lösung erklärt. Die Factory-Methode liefert den gleichen Effekt wie das Argument für die Factory-Methode. Dadurch wird eine neue Variable mit einem für das Lambda lokalen Gültigkeitsbereich erstellt. Die angegebene Lösung funktioniert jedoch nicht syntaktisch, da es keine Argumente für das Lambda gibt - und die unten stehende Lambda-in-Lamda-Lösung liefert den gleichen Effekt, ohne eine neue dauerhafte Methode zum Erstellen eines Lambda zu erstellen
Mark Parris
130

Wenn ein Lambda erstellt wird, wird keine Kopie der Variablen in dem von ihm verwendeten umschließenden Bereich erstellt. Es wird ein Verweis auf die Umgebung beibehalten, damit der Wert der Variablen später nachgeschlagen werden kann. Es gibt nur einen m. Es wird jedes Mal durch die Schleife zugewiesen. Nach der Schleife hat die Variable meinen Wert 'mi'. Wenn Sie also die später erstellte Funktion tatsächlich ausführen, wird der Wert min der Umgebung, in der sie erstellt wurde, nachgeschlagen, die dann einen Wert hat 'mi'.

Eine übliche und idiomatische Lösung für dieses Problem besteht darin, den Wert mzum Zeitpunkt der Erstellung des Lambda zu erfassen, indem es als Standardargument eines optionalen Parameters verwendet wird. Normalerweise verwenden Sie einen gleichnamigen Parameter, damit Sie den Code nicht ändern müssen:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))
newacct
quelle
Ich meine optionale Parameter mit Standardwerten
lispmachine
6
Schöne Lösung! Obwohl es schwierig ist, finde ich, dass die ursprüngliche Bedeutung klarer ist als bei anderen Syntaxen.
Quantum7
3
Daran ist überhaupt nichts Hackisches oder Kniffliges; Es ist genau die gleiche Lösung, die in den offiziellen Python-FAQ vorgeschlagen wird. Siehe hier .
abarnert
3
@abernert, "hackisch und knifflig" ist nicht unbedingt unvereinbar mit "die Lösung, die die offiziellen Python-FAQ vorschlagen". Danke für den Hinweis.
Don Hatch
1
Die Wiederverwendung des gleichen Variablennamens ist jemandem unklar, der mit diesem Konzept nicht vertraut ist. Illustration wäre besser, wenn es Lambda n = m wäre. Ja, Sie müssten Ihren Rückrufparameter ändern, aber der for-Schleifenkörper könnte meiner Meinung nach gleich bleiben.
Nick
6

Python verwendet natürlich Referenzen, aber das spielt in diesem Zusammenhang keine Rolle.

Wenn Sie ein Lambda (oder eine Funktion, da dies genau dasselbe Verhalten ist) definieren, wird der Lambda-Ausdruck vor der Laufzeit nicht ausgewertet:

# defining that function is perfectly fine
def broken():
    print undefined_var

broken() # but calling it will raise a NameError

Noch überraschender als Ihr Lambda-Beispiel:

i = 'bar'
def foo():
    print i

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'

Kurz gesagt, denken Sie dynamisch: Vor der Interpretation wird nichts ausgewertet. Deshalb verwendet Ihr Code den neuesten Wert von m.

Wenn in der Lambda-Ausführung nach m gesucht wird, wird m aus dem obersten Bereich genommen, was bedeutet, dass, wie andere betonten; Sie können dieses Problem umgehen, indem Sie einen weiteren Bereich hinzufügen:

def factory(x):
    return lambda: callback(x)

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))

Wenn hier das Lambda aufgerufen wird, sucht es im Definitionsbereich des Lambda nach einem x. Dieses x ist eine lokale Variable, die im Fabrikkörper definiert ist. Aus diesem Grund ist der Wert, der bei der Lambda-Ausführung verwendet wird, der Wert, der beim Aufruf der Factory als Parameter übergeben wurde. Und Doremi!

Als Hinweis hätte ich Factory als Factory (m) definieren können [x durch m ersetzen], das Verhalten ist das gleiche. Ich habe aus Gründen der Klarheit einen anderen Namen verwendet :)

Sie könnten feststellen, dass Andrej Bauer ähnliche Lambda-Probleme hatte. Was in diesem Blog interessant ist, sind die Kommentare, in denen Sie mehr über das Schließen von Python erfahren werden :)

Nicolas Dumazet
quelle
1

Nicht direkt mit dem vorliegenden Thema verbunden, aber dennoch ein unschätzbares Stück Weisheit: Python Objects von Fredrik Lundh.

tzot
quelle
1
Nicht direkt mit Ihrer Antwort verbunden, aber eine Suche nach Kätzchen: google.com/search?q=kitten
Singletoned
@Singletoned: Wenn das OP den Artikel, zu dem ich einen Link bereitgestellt habe, durchsucht hätte, würden sie die Frage überhaupt nicht stellen. deshalb ist es indirekt verwandt. Ich bin sicher, Sie werden erfreut sein, mir zu erklären, wie Kätzchen indirekt mit meiner Antwort zusammenhängen (
vermutlich
1

Ja, das ist ein Problem des Umfangs, es bindet an das äußere m, egal ob Sie ein Lambda oder eine lokale Funktion verwenden. Verwenden Sie stattdessen einen Funktor:

class Func1(object):
    def __init__(self, callback, message):
        self.callback = callback
        self.message = message
    def __call__(self):
        return self.callback(self.message)
funcList.append(Func1(callback, m))
Benoît
quelle
1

Die Lösung für Lambda ist mehr Lambda

In [0]: funcs = [(lambda j: (lambda: j))(i) for i in ('do', 're', 'mi')]

In [1]: funcs
Out[1]: 
[<function __main__.<lambda>>,
 <function __main__.<lambda>>,
 <function __main__.<lambda>>]

In [2]: [f() for f in funcs]
Out[2]: ['do', 're', 'mi']

Das Äußere lambdawird verwendet, um den aktuellen Wert von ian j an zu binden

Jedesmal , wenn die äußere lambdaes genannt wird , macht eine Instanz des inneren lambdamit jdem aktuellen Wert von gebundenen ials i‚S - Wert

Aaron Goldman
quelle
0

Erstens ist das, was Sie sehen, kein Problem und bezieht sich nicht auf Call-by-Reference oder By-Value.

Die von Ihnen definierte Lambda-Syntax enthält keine Parameter. Daher liegt der Bereich, den Sie mit den Parametern sehen, maußerhalb der Lambda-Funktion. Aus diesem Grund sehen Sie diese Ergebnisse.

Die Lambda-Syntax ist in Ihrem Beispiel nicht erforderlich, und Sie möchten lieber einen einfachen Funktionsaufruf verwenden:

for m in ('do', 're', 'mi'):
    callback(m)

Auch hier sollten Sie sehr genau wissen, welche Lambda-Parameter Sie verwenden und wo genau ihr Umfang beginnt und endet.

Als Randnotiz zur Parameterübergabe. Parameter in Python sind immer Verweise auf Objekte. Um Alex Martelli zu zitieren:

Das Terminologieproblem kann auf die Tatsache zurückzuführen sein, dass der Wert eines Namens in Python eine Referenz auf ein Objekt ist. Sie übergeben also immer den Wert (kein implizites Kopieren), und dieser Wert ist immer eine Referenz. [...] Wenn Sie nun einen Namen dafür prägen möchten, z. B. "nach Objektreferenz", "nach nicht kopiertem Wert" oder was auch immer, seien Sie mein Gast. Der Versuch, Terminologie, die allgemeiner auf Sprachen angewendet wird, in denen "Variablen Kästchen sind", in einer Sprache wiederzuverwenden, in der "Variablen Post-It-Tags sind", ist meiner Meinung nach eher verwirrend als hilfreich.

Yuval Adam
quelle
0

Die Variable mwird erfasst, sodass Ihr Lambda-Ausdruck immer seinen "aktuellen" Wert sieht.

Wenn Sie den Wert zu einem bestimmten Zeitpunkt effektiv erfassen müssen, schreiben Sie eine Funktion, indem Sie den gewünschten Wert als Parameter verwenden und einen Lambda-Ausdruck zurückgeben. Zu diesem Zeitpunkt erfasst das Lambda den Wert des Parameters , der sich nicht ändert, wenn Sie die Funktion mehrmals aufrufen:

def callback(msg):
    print msg

def createCallback(msg):
    return lambda: callback(msg)

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(createCallback(m))
for f in funcList:
    f()

Ausgabe:

do
re
mi
Jon Skeet
quelle
0

In Python gibt es eigentlich keine Variablen im klassischen Sinne, sondern nur Namen, die durch Verweise auf das betreffende Objekt gebunden wurden. Sogar Funktionen sind in Python eine Art Objekt, und Lambdas machen keine Ausnahme von der Regel :)

Tom
quelle
Wenn Sie "im klassischen Sinne" sagen, meinen Sie "wie C". Viele Sprachen, einschließlich Python, implementieren Variablen anders als C.
Ned Batchelder
0

Als Randnotiz maperzwingt, obwohl von einer bekannten Python-Figur verachtet, eine Konstruktion, die diese Falle verhindert.

fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])

NB: Der erste lambda iverhält sich wie die Fabrik in anderen Antworten.

YvesgereY
quelle