L'explication
Le problème ici est que la valeur de in'est pas enregistrée lorsque la fonction fest créée. Au contraire, frecherche la valeur du imoment 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:
global_var = 'foo'
def my_function():
print(global_var)
global_var = 'bar'
my_function()
Lorsque vous lisez ce code, vous vous attendez - bien sûr - à ce qu'il affiche "bar", pas "foo", car la valeur de global_vara changé après la déclaration de la fonction. La même chose se produit dans votre propre code: au moment où vous appelez f, la valeur de ia changé et a été définie sur 2.
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 ien l'utilisant comme argument par défaut
Contrairement aux variables de fermeture (comme i), les arguments par défaut sont évalués immédiatement lorsque la fonction est définie:
for i in range(3):
def f(i=i): # <- right here is the important bit
return i
functions.append(f)
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 iest instantané et sauvegardée.
>>> i = 0
>>> def f(i=i):
... pass
>>> f.__defaults__ # this is where the current value of i is stored
(0,)
>>> # assigning a new value to i has no effect on the function's default arguments
>>> i = 5
>>> f.__defaults__
(0,)
Utiliser une fabrique de fonctions pour capturer la valeur actuelle de idans une fermeture
La racine de votre problème est qu'il is'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 :
def f_factory(i):
def f():
return i # i is now a *local* variable of f_factory and can't ever change
return f
for i in range(3):
f = f_factory(i)
functions.append(f)
Utilisez functools.partialpour lier la valeur actuelle de iàf
functools.partialvous permet d'attacher des arguments à une fonction existante. D'une certaine manière, c'est aussi une sorte d'usine de fonctions.
import functools
def f(i):
return i
for i in range(3):
f_with_i = functools.partial(f, i) # important: use a different variable than "f"
functions.append(f_with_i)
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:
>>> i = [] # instead of an int, i is now a *mutable* object
>>> def f(i=i):
... print('i =', i)
...
>>> i.append(5) # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]
Remarquez à quel point il a iencore changé même si nous l'avons transformé en argument par défaut! Si votre code mute i , vous devez lier une copie de ià votre fonction, comme ceci:
def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())