82 votes

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

J'ai besoin d'une fonction de rappel qui soit presque exactement la même pour une série d'événements gui. La fonction se comportera légèrement différemment en fonction de l'événement qui l'a appelée. Cela me semble être un cas simple, mais je n'arrive pas à comprendre ce comportement bizarre des fonctions lambda.

J'ai donc le code simplifié suivant :

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

Je m'y attendais :

do
re
mi
do
re
mi

Pourquoi l'utilisation d'un itérateur a perturbé 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.

126voto

newacct Points 42530

Lorsqu'une lambda est créée, elle ne fait pas de copie des variables de la portée englobante qu'elle utilise. Elle maintient une référence à l'environnement afin de pouvoir rechercher la valeur de la variable plus tard. Il n'y a qu'une seule m . Il est assigné à chaque fois dans la boucle. Après la boucle, la variable m a une valeur 'mi' . Ainsi, lorsque vous exécuterez la fonction que vous aurez créée plus tard, elle cherchera la valeur de m dans l'environnement qui l'a créé, qui aura alors une valeur 'mi' .

Une solution commune et idiomatique à ce problème est de capturer la valeur de m au moment où le lambda est créé en l'utilisant comme argument par défaut d'un paramètre optionnel. On utilise 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))

73voto

lispmachine Points 2379

Le problème ici est que m la variable (une référence) étant 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 trouver m mais ce cette fois, c'est callback_factory qui est créé une fois par chaque callback_factory appeler.

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

6voto

NicDumZ Points 5566

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, puisque c'est exactement le même comportement), l'expression lambda n'est pas évaluée 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'

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

Lorsqu'il cherche m dans l'exécution de la lambda, m est pris dans la portée la plus élevée, ce qui signifie que, comme d'autres l'ont fait remarquer, 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 un x dans la portée de la définition du lambda. Ce x est une variable locale définie dans le corps de factory. De ce fait, la valeur utilisée lors de l'exécution de la lambda sera la valeur qui a été passée en paramètre lors de l'appel à factory. Et doremi !

A titre d'information, 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 pourriez trouver que Andrej Bauer J'ai des problèmes similaires de lambda. Ce qui est intéressant sur ce blog, ce sont les commentaires, où vous en apprendrez plus sur la fermeture de python :)

1voto

tzot Points 32224

Ce n'est pas directement lié à la question qui nous occupe, mais c'est néanmoins une sagesse inestimable : Objets Python par Fredrik Lundh.

0voto

Yuval Adam Points 59423

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 m est externe à la fonction lambda. C'est pourquoi vous obtenez 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 quant aux paramètres lambda que vous utilisez et à l'endroit exact où leur portée commence et se termine.

À titre d'information, concernant le passage des 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 un nom est une référence à un objet. Ainsi, vous passez toujours la valeur (pas de pas de copie implicite), et cette valeur est toujours toujours une référence. [...] Maintenant, si vous voulez inventer un nom pour cela, tel que "par référence à un objet", "par valeur non copiée", ou autre, je vous en prie. Essayer de réutiliser une terminologie qui est plus généralement appliquée aux langages où les "variables sont des boîtes" à un langage où les "variables sont des post-it post-it" est, à mon avis, plus susceptible de confondre que d'aider.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X