La explicación
El problema aquí es que el valor de i
no se guarda cuando f
se crea la función . Más bien, f
busca el valor de i
cuá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_var
ha 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 i
ha 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 i
usá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 i
en un cierre
La raíz de su problema es que i
es 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.partial
para vincular el valor actual de i
af
functools.partial
le 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 i
aún cambió a pesar de que lo convertimos en un argumento predeterminado! Si su código muta i
, entonces debe vincular una copia de i
a su función, así:
def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())