La explicación
El problema aquí es que el valor de ino se guarda cuando fse crea la función . Más bien, fbusca el valor de icuándo se llama .
Si lo piensas, este comportamiento tiene mucho sentido. De hecho, es la única forma razonable en que pueden funcionar las funciones. Imagina que tienes una función que accede a una variable global, como esta:
global_var = 'foo'
def my_function():
print(global_var)
global_var = 'bar'
my_function()
Cuando lea este código, por supuesto, esperaría que imprima "bar", no "foo", porque el valor de global_varha cambiado después de que se declaró la función. Lo mismo sucede en su propio código: en el momento en que llama f, el valor de iha cambiado y se ha establecido en 2.
La solución
En realidad, hay muchas formas de resolver este problema. Aquí hay algunas opciones:
Forzar la vinculación anticipada de iusándolo como argumento predeterminado
A diferencia de las variables de cierre (como i), los argumentos predeterminados se evalúan inmediatamente cuando se define la función:
for i in range(3):
def f(i=i): # <- right here is the important bit
return i
functions.append(f)
Para dar una idea de cómo / por qué funciona esto: Los argumentos predeterminados de una función se almacenan como un atributo de la función; por lo tanto, se toma una instantánea y se guarda el valor actual de i.
>>> 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,)
Utilice una función de fábrica para capturar el valor actual de ien un cierre
La raíz de su problema es que ies una variable que puede cambiar. Podemos solucionar este problema creando otra variable que se garantiza que nunca cambiará, y la forma más fácil de hacerlo es un cierre :
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)
Úselo functools.partialpara vincular el valor actual de iaf
functools.partialle permite adjuntar argumentos a una función existente. En cierto modo, también es una especie de fábrica de funciones.
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)
Advertencia: estas soluciones solo funcionan si asigna un nuevo valor a la variable. Si modifica el objeto almacenado en la variable, experimentará el mismo problema nuevamente:
>>> 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]
¡Observe cómo iaún cambió a pesar de que lo convertimos en un argumento predeterminado! Si su código muta i , entonces debe vincular una copia de ia su función, así:
def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())