La spiegazione
Il problema qui è che il valore di i
non viene salvato quando f
viene creata la funzione . Piuttosto, f
cerca il valore di i
quando viene chiamato .
Se ci pensi, questo comportamento ha perfettamente senso. In effetti, è l'unico modo ragionevole in cui le funzioni possono funzionare. Immagina di avere una funzione che accede a una variabile globale, come questa:
global_var = 'foo'
def my_function():
print(global_var)
global_var = 'bar'
my_function()
Quando leggete questo codice, vi aspettereste - ovviamente - che stampi "bar", non "foo", perché il valore di global_var
è cambiato dopo che la funzione è stata dichiarata. La stessa cosa sta accadendo nel tuo codice: nel momento in cui chiami f
, il valore di i
è cambiato ed è stato impostato su2
.
La soluzione
In realtà ci sono molti modi per risolvere questo problema. Ecco alcune opzioni:
Forza l'associazione anticipata di i
utilizzandolo come argomento predefinito
A differenza delle variabili di chiusura (come i
), gli argomenti predefiniti vengono valutati immediatamente quando la funzione viene definita:
for i in range(3):
def f(i=i): # <- right here is the important bit
return i
functions.append(f)
Per dare un po 'di intuizione su come / perché funziona: Gli argomenti predefiniti di una funzione sono memorizzati come attributo della funzione; in questo modo il valore corrente di i
viene acquisito e salvato.
>>> 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,)
Utilizzare una factory di funzioni per acquisire il valore corrente di i
in una chiusura
La radice del tuo problema è che i
è una variabile che può cambiare. Possiamo aggirare questo problema creando un'altra variabile che è garantito che non cambierà mai e il modo più semplice per farlo è una chiusura :
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)
Utilizzare functools.partial
per associare il valore corrente di i
af
functools.partial
consente di allegare argomenti a una funzione esistente. In un certo senso, anch'essa è una specie di fabbrica di funzioni.
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)
Avvertenza: queste soluzioni funzionano solo se si assegna un nuovo valore alla variabile. Se modifichi l'oggetto memorizzato nella variabile, riscontrerai di nuovo lo stesso problema:
>>> 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]
Nota come è i
ancora cambiato anche se lo abbiamo trasformato in un argomento predefinito! Se il tuo codice muta i
, devi associare una copia di i
alla tua funzione, in questo modo:
def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())