J'ai besoin d'une fonction de rappel qui est presque exactement la même pour une série d'événements gui. 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 tout gâché?
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 a le même problème.
Le problème ici est que la variable m
(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 étendue 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 l'étendue environnante pour trouver m
, mais cette fois c'est callback_factory
portée qui est créée une fois tous les 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()
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 la valeur de la variable ultérieurement. Il n'y a qu'un m
. Il est attribué à chaque fois dans la boucle. Après la boucle, la variable m
a la valeur 'mi'
. Ainsi, lorsque vous exécutez réellement la fonction que vous avez créée plus tard, elle recherchera la valeur de m
dans l'environnement qui l'a créée, qui aura alors la valeur 'mi'
.
Une solution courante et idiomatique à ce problème consiste à capturer la valeur de m
au 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 afin de ne pas avoir à modifier le corps du code:
for m in ('do', 're', 'mi'):
funcList.append(lambda m=m: callback(m))
Python utilise des références bien sûr, mais cela n'a pas d'importance dans ce contexte.
Lorsque vous définissez un lambda (ou une fonction, car 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'
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 recherche m dans l'exécution lambda, m est pris dans la portée la plus haute, ce qui signifie que, comme d'autres l'ont souligné; vous pouvez contourner ce problème en ajoutant une autre étendue:
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 définition du 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 de lambda sera la valeur transmise en tant que paramètre lors de l'appel à l'usine. Et doremi!
Comme note, j'aurais pu définir l'usine comme usine (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 a des problèmes lambda similaires. Ce qui est intéressant sur ce blog, ce sont les commentaires, où vous en apprendrez plus sur python fermeture :)
Pas directement lié au problème en question, mais une sagesse inestimable néanmoins: Objets Python par Fredrik Lundh.
il n'y a en fait aucune variable au sens classique de 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 exception à la règle :)
le 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']
lambda
externe est utilisé pour lier la valeur actuelle de i
à j
au niveau
chaque fois que le lambda
externe est appelé, il crée une instance du lambda
interne avec j
lié à la valeur actuelle de i
comme i
valeur
En remarque, map
, bien que méprisé par une figure bien connue Python, force une construction qui empêche cet écueil.
fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])
NB: le premier lambda i
agit comme l'usine dans les autres réponses.
Tout d'abord, 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 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 des paramètres. Les paramètres dans 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 passez toujours la valeur (pas de copie implicite), et cette valeur est toujours une référence. [...] Maintenant, si vous voulez inventer un nom pour cela, comme "par référence d'objet", "par valeur non copiée", ou quoi que ce soit, soyez mon invité. Essayer de réutiliser une 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 confondre que d'aider.
La variable m
est capturée, donc votre expression lambda voit toujours sa valeur "actuelle".
Si vous avez besoin de capturer efficacement la valeur à un moment donné, écrivez une fonction 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
Oui, c'est un problème de portée, il se lie au m externe, que vous utilisiez un lambda ou une fonction locale. Utilisez plutôt 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))