J'essaye de créer des fonctions à l'intérieur d'une boucle:
functions = []
for i in range(3):
def f():
return i
# alternatively: f = lambda: i
functions.append(f)
Le problème est que toutes les fonctions finissent par être les mêmes. Au lieu de renvoyer 0, 1 et 2, les trois fonctions renvoient 2:
print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output: [2, 2, 2]
Pourquoi cela se produit-il et que dois-je faire pour obtenir 3 fonctions différentes qui produisent respectivement 0, 1 et 2?
Réponses:
Vous rencontrez un problème de liaison tardive - chaque fonction recherche le
i
plus tard possible (ainsi, lorsqu'elle est appelée après la fin de la boucle,i
elle sera définie sur2
).Facilement corrigé en forçant la liaison anticipée: changez
def f():
pourdef f(i=i):
ceci:Les valeurs par défaut (la droite
i
dansi=i
une valeur par défaut pour le nom de l' argumenti
, qui est la main gauchei
eni=i
) sont cherchés àdef
temps, pas lecall
temps, donc essentiellement ils sont un moyen de recherche spécifiquement pour la liaison précoce.Si vous craignez d'
f
obtenir un argument supplémentaire (et donc potentiellement d'être appelé à tort), il existe une manière plus sophistiquée qui impliquait d'utiliser une fermeture comme une "fabrique de fonctions":et dans votre boucle, utilisez
f = make_f(i)
plutôt que l'def
instruction.la source
L'explication
Le problème ici est que la valeur de
i
n'est pas enregistrée lorsque la fonctionf
est créée. Au contraire,f
recherche la valeur dui
moment où il est appelé .Si vous y réfléchissez, ce comportement est parfaitement logique. En fait, c'est le seul moyen raisonnable pour les fonctions de fonctionner. Imaginez que vous ayez une fonction qui accède à une variable globale, comme ceci:
Lorsque vous lisez ce code, vous vous attendez - bien sûr - à ce qu'il affiche "bar", pas "foo", car la valeur de
global_var
a changé après la déclaration de la fonction. La même chose se produit dans votre propre code: au moment où vous appelezf
, la valeur dei
a changé et a été définie sur2
.La solution
Il existe en fait de nombreuses façons de résoudre ce problème. Voici quelques options:
Forcer la liaison anticipée de
i
en l'utilisant comme argument par défautContrairement aux variables de fermeture (comme
i
), les arguments par défaut sont évalués immédiatement lorsque la fonction est définie:Pour donner un petit aperçu de comment / pourquoi cela fonctionne: Les arguments par défaut d'une fonction sont stockés en tant qu'attribut de la fonction; ainsi la valeur actuelle de
i
est instantané et sauvegardée.Utiliser une fabrique de fonctions pour capturer la valeur actuelle de
i
dans une fermetureLa racine de votre problème est qu'il
i
s'agit d'une variable qui peut changer. Nous pouvons contourner ce problème en créant une autre variable qui est garantie de ne jamais changer - et le moyen le plus simple de le faire est une fermeture :Utilisez
functools.partial
pour lier la valeur actuelle dei
àf
functools.partial
vous permet d'attacher des arguments à une fonction existante. D'une certaine manière, c'est aussi une sorte d'usine de fonctions.Attention: ces solutions ne fonctionnent que si vous attribuez une nouvelle valeur à la variable. Si vous modifiez l'objet stocké dans la variable, vous rencontrerez à nouveau le même problème:
Remarquez à quel point il a
i
encore changé même si nous l'avons transformé en argument par défaut! Si votre code mutei
, vous devez lier une copie dei
à votre fonction, comme ceci:def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())
la source