web-dev-qa-db-fra.com

Python Lambda dans une boucle

Considérant l'extrait de code suivant:

# directorys == {'login': <object at ...>, 'home': <object at ...>}
for d in directorys:
    self.command["cd " + d] = (lambda : self.root.change_directory(d))

Je m'attends à créer un dictionnaire de deux fonctions comme suit:

# Expected :
self.command == {
    "cd login": lambda: self.root.change_directory("login"),
    "cd home": lambda: self.root.change_directory("home")
}

mais il semble que les deux fonctions lambda générées soient exactement les mêmes:

# Result :
self.command == {
    "cd login": lambda: self.root.change_directory("login"),
    "cd home": lambda: self.root.change_directory("login")   # <- Why login ?
}

Je ne comprends vraiment pas pourquoi. Avez-vous des suggestions ?

52
FunkySayu

Vous devez lier d pour chaque fonction créée. Une façon de le faire est de le passer en tant que paramètre avec une valeur par défaut:

lambda d=d: self.root.change_directory(d)

Maintenant, le d à l'intérieur de la fonction utilise le paramètre, même s'il a le même nom, et la valeur par défaut pour cela est évaluée lorsque la fonction est créée. Pour vous aider à voir ceci:

lambda bound_d=d: self.root.change_directory(bound_d)

N'oubliez pas le fonctionnement des valeurs par défaut, comme pour les objets modifiables comme les listes et les dict, car vous liez un objet.

Cet idiome de paramètres avec des valeurs par défaut est assez commun, mais peut échouer si vous introspectez les paramètres de fonction et déterminez quoi faire en fonction de leur présence. Vous pouvez éviter le paramètre avec une autre fermeture:

(lambda d=d: lambda: self.root.change_directory(d))()
# or
(lambda d: lambda: self.root.change_directory(d))(d)
69
Roger Pate

Cela est dû au point auquel d est lié. Les fonctions lambda pointent toutes sur la variable d plutôt que sur la valeur actuelle de celui-ci, donc lorsque vous mettez à jour d dans la prochaine itération, cette mise à jour est visible dans toutes vos fonctions.

Pour un exemple plus simple:

funcs = []
for x in [1,2,3]:
  funcs.append(lambda: x)

for f in funcs:
  print f()

# output:
3
3
3

Vous pouvez contourner cela en ajoutant une fonction supplémentaire, comme ceci:

def makeFunc(x):
  return lambda: x

funcs = []
for x in [1,2,3]:
  funcs.append(makeFunc(x))

for f in funcs:
  print f()

# output:
1
2
3

Vous pouvez également corriger la portée à l'intérieur de l'expression lambda

lambda bound_x=x: bound_x

Cependant en général c'est pas une bonne pratique car vous avez changé la signature de votre fonction.

20
robbie_c

J'ai rencontré le même problème. La solution choisie m'a beaucoup aidé, mais je considère nécessaire d'ajouter une précision pour rendre fonctionnel le code de la question: définir la fonction lambda en dehors de la boucle. Soit dit en passant, la valeur par défaut n'est pas nécessaire.

foo = lambda d: lambda : self.root.change_directory(d)
for d in directorys:
    self.command["cd " + d] = (foo(d))

Alternativement, au lieu de lambda, vous pouvez utiliser functools.partial qui, à mon avis, a une syntaxe plus propre.

Au lieu de:

for d in directorys:
    self.command["cd " + d] = (lambda d=d: self.root.change_directory(d))

ce sera:

for d in directorys:
    self.command["cd " + d] = partial(self.root.change_directory, d)

Ou, voici un autre exemple simple:

numbers = [1, 2, 3]

lambdas = [lambda: print(number) 
           for number in numbers]
lambdas_with_binding = [lambda number=number: print(number) 
                        for number in numbers]
partials = [partial(print, number) 
            for number in numbers]

for function in lambdas:
    function()
# 3 3 3
for function in lambdas_with_binding:
    function()
# 1 2 3
for function in partials:
    function()
# 1 2 3
0
Georgy