Portée des fonctions lambda et leurs paramètres?


89

J'ai besoin d'une fonction de rappel qui est presque exactement la même pour une série d'événements d'interface graphique. La fonction se comportera légèrement différemment selon l'événement qui l'a appelée. Cela me semble être un cas simple, mais je ne peux pas comprendre ce comportement étrange des fonctions lambda.

J'ai donc le code simplifié suivant ci-dessous:

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()

La sortie de ce code est:

mi
mi
mi
do
re
mi

J'esperais:

do
re
mi
do
re
mi

Pourquoi l'utilisation d'un itérateur a-t-il gâché les choses?

J'ai essayé d'utiliser une copie profonde:

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

Mais cela pose le même problème.


3
Le titre de votre question est quelque peu trompeur.
lispmachine

1
Pourquoi utiliser des lambdas si vous les trouvez déroutants? Pourquoi ne pas utiliser def pour définir des fonctions? Qu'est-ce qui rend les lambdas si importantes dans votre problème?
S.Lott le

La fonction imbriquée @ S.Lott entraînera le même problème (peut-être plus clairement visible)
lispmachine

1
@agartland: Êtes-vous moi? Moi aussi je travaillais sur des événements GUI, et j'ai écrit le test presque identique suivant avant de trouver cette page pendant la recherche de fond: pastebin.com/M5jjHjFT
imallett

5
Voir Pourquoi les lambdas définis dans une boucle avec des valeurs différentes renvoient-ils tous le même résultat? dans la FAQ officielle de programmation pour Python. Cela explique assez bien le problème et offre une solution.
abarnert

Réponses:


79

Le problème ici est que la mvariable (une référence) est prise dans la portée environnante. Seuls les paramètres sont conservés dans la portée lambda.

Pour résoudre ce problème, vous devez créer une autre portée pour lambda:

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()

Dans l'exemple ci-dessus, lambda utilise également la portée environnante pour rechercher m, mais cette fois, c'est la callback_factoryportée qui est créée une fois par callback_factory appel.

Ou avec 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()

2
Cette explication est un peu trompeuse. Le problème est le changement de valeur de m dans l'itération, pas la portée.
Ixx

Le commentaire ci-dessus est vrai comme noté par @abarnert dans le commentaire sur la question où un lien est également donné qui explique le phénonimon et la solution. La méthode de fabrique fournit le même effet que l'argument de la méthode de fabrique a pour effet de créer une nouvelle variable avec une portée locale au lambda. Cependant, la solution donnée ne fonctionne pas de manière syntatcique car il n'y a pas d'arguments pour le lambda - et la solution lambda dans lamda ci-dessous fournit également le même effet sans créer une nouvelle méthode persistante pour créer un lambda
Mark Parris

132

Lorsqu'un lambda est créé, il ne fait pas de copie des variables dans la portée englobante qu'il utilise. Il conserve une référence à l'environnement afin de pouvoir rechercher ultérieurement la valeur de la variable. Il n'y en a qu'un m. Il est assigné à chaque fois dans la boucle. Après la boucle, la variable ma une valeur 'mi'. Ainsi, lorsque vous exécutez réellement la fonction que vous avez créée plus tard, elle recherchera la valeur de mdans l'environnement qui l'a créée, qui aura alors de la valeur 'mi'.

Une solution courante et idiomatique à ce problème consiste à capturer la valeur de mau moment où le lambda est créé en l'utilisant comme argument par défaut d'un paramètre facultatif. Vous utilisez généralement un paramètre du même nom pour ne pas avoir à modifier le corps du code:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))

je veux dire des paramètres optionnels avec des valeurs par défaut
lispmachine

6
Belle solution! Bien que délicat, je pense que la signification originale est plus claire qu'avec d'autres syntaxes.
Quantum7

3
Il n'y a rien du tout hackish ou délicat à ce sujet; c'est exactement la même solution suggérée par la FAQ officielle Python. Regardez ici .
abarnert

3
@abernert, "hackish and tricky" n'est pas nécessairement incompatible avec "être la solution suggérée par la FAQ officielle de Python". Merci pour la référence.
Don Hatch

1
réutiliser le même nom de variable n'est pas clair pour quelqu'un qui ne connaît pas ce concept. L'illustration serait meilleure si c'était lambda n = m. Oui, vous devrez changer votre paramètre de rappel, mais le corps de la boucle for pourrait rester le même, je pense.
Nick

6

Python utilise bien sûr des références, mais cela n'a pas d'importance dans ce contexte.

Lorsque vous définissez un lambda (ou une fonction, puisqu'il s'agit exactement du même comportement), il n'évalue pas l'expression lambda avant l'exécution:

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

broken() # but calling it will raise a NameError

Encore plus surprenant que votre exemple lambda:

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

foo() # bar

i = 'banana'

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

Bref, pensez dynamique: rien n'est évalué avant interprétation, c'est pourquoi votre code utilise la dernière valeur de m.

Lorsqu'il recherche m dans l'exécution lambda, m est pris dans la portée la plus élevée, ce qui signifie que, comme d'autres l'ont souligné; vous pouvez contourner ce problème en ajoutant une autre portée:

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

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

Ici, lorsque le lambda est appelé, il recherche dans la portée de la définition lambda un x. Ce x est une variable locale définie dans le corps de l'usine. Pour cette raison, la valeur utilisée lors de l'exécution lambda sera la valeur qui a été transmise en tant que paramètre lors de l'appel à l'usine. Et doremi!

Pour mémoire, j'aurais pu définir factory comme factory (m) [remplacer x par m], le comportement est le même. J'ai utilisé un nom différent pour plus de clarté :)

Vous constaterez peut-être qu'Andrej Bauer a des problèmes lambda similaires. Ce qui est intéressant sur ce blog, ce sont les commentaires, où vous en apprendrez plus sur la fermeture de python :)


1

Pas directement lié au problème en question, mais néanmoins un élément de sagesse inestimable: les objets Python de Fredrik Lundh.


1
Pas directement lié à votre réponse, mais une recherche de chatons: google.com/search?q=kitten
Singletoned

@Singletoned: si l'OP a grogné l'article vers lequel j'ai fourni un lien, il ne poserait pas la question en premier lieu; c'est pourquoi c'est indirectement lié. Je suis sûr que vous serez ravi de m'expliquer comment les chatons sont indirectement liés à ma réponse (à travers une approche holistique, je présume;)
tzot

1

Oui, c'est un problème de portée, il se lie au m extérieur, que vous utilisiez un lambda ou une fonction locale. À la place, utilisez un foncteur:

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))

1

la soluiton à lambda est plus 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']

l'externe lambdaest utilisé pour lier la valeur actuelle de ià j au

chaque fois que l'extérieur lambdaest appelée , elle fait une instance de l'intérieur lambdaavec jlié à la valeur courante de ique ila valeur de »


0

Premièrement, ce que vous voyez n'est pas un problème et n'est pas lié à l'appel par référence ou par valeur.

La syntaxe lambda que vous avez définie n'a pas de paramètres et, en tant que telle, la portée que vous voyez avec le paramètre mest externe à la fonction lambda. C'est pourquoi vous voyez ces résultats.

La syntaxe Lambda, dans votre exemple n'est pas nécessaire, et vous préférez utiliser un simple appel de fonction:

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

Encore une fois, vous devez être très précis sur les paramètres lambda que vous utilisez et où exactement leur portée commence et se termine.

En remarque, concernant le passage de paramètres. Les paramètres en python sont toujours des références à des objets. Pour citer Alex Martelli:

Le problème de terminologie peut être dû au fait qu'en python, la valeur d'un nom est une référence à un objet. Ainsi, vous transmettez toujours la valeur (pas de copie implicite), et cette valeur est toujours une référence. [...] Maintenant, si vous voulez créer un nom pour cela, comme "par référence d'objet", "par valeur non copiée", ou autre, soyez mon invité. Essayer de réutiliser la terminologie qui est plus généralement appliquée aux langues où «les variables sont des boîtes» à une langue où «les variables sont des balises post-it» est, à mon humble avis, plus susceptible de semer la confusion que d'aider.


0

La variable mest capturée, donc votre expression lambda voit toujours sa valeur «actuelle».

Si vous avez besoin de capturer efficacement la valeur à un moment donné, write a function prend la valeur souhaitée comme paramètre et renvoie une expression lambda. À ce stade, le lambda capturera la valeur du paramètre , qui ne changera pas lorsque vous appelez la fonction plusieurs fois:

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()

Production:

do
re
mi

0

il n'y a en fait aucune variable au sens classique du terme en Python, juste des noms qui ont été liés par des références à l'objet applicable. Même les fonctions sont une sorte d'objet en Python, et les lambdas ne font pas d'exception à la règle :)


Quand vous dites «au sens classique du terme», vous voulez dire «comme C l'a fait». De nombreux langages, y compris Python, implémentent les variables différemment de C.
Ned Batchelder

0

En passant, mapbien que méprisé par une figure bien connue de Python, force une construction qui empêche cet écueil.

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

NB: le premier lambda iagit comme l'usine dans d'autres réponses.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.