L'explication
Le problème ici est que la valeur de i
n'est pas enregistrée lorsque la fonction f
est créée. Au contraire, f
recherche la valeur du i
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:
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_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 appelez f
, la valeur de i
a 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 i
en 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 i
est 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 i
dans une fermeture
La 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 :
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.partial
pour lier la valeur actuelle de i
à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.
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 i
encore 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())