web-dev-qa-db-fra.com

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

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.

76
agartland

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

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

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

6
Nicolas Dumazet

Pas directement lié au problème en question, mais une sagesse inestimable néanmoins: Objets Python par Fredrik Lundh.

1
tzot

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

0
Tom

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

0
Aaron Goldman

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.

0
YvesgereY

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.

0
Yuval Adam

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
0
Jon Skeet

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))
0
Benoît