Estoy tratando de crear funciones dentro de un bucle:
functions = []
for i in range(3):
def f():
return i
# alternatively: f = lambda: i
functions.append(f)
El problema es que todas las funciones terminan siendo iguales. En lugar de devolver 0, 1 y 2, las tres funciones devuelven 2:
print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output: [2, 2, 2]
¿Por qué sucede esto y qué debo hacer para obtener 3 funciones diferentes que generen 0, 1 y 2 respectivamente?
Respuestas:
Tiene un problema con el enlace tardío : cada función busca lo más
i
tarde posible (por lo tanto, cuando se llama después del final del ciclo,i
se configurará en2
).Se soluciona fácilmente forzando el enlace anticipado: cambie
def f():
adef f(i=i):
esto:Los valores por defecto (la mano derecha
i
eni=i
un valor por defecto para el nombre de argumentoi
, que es el de la izquierdai
eni=i
) se buscan endef
el tiempo, no en elcall
tiempo, lo que en esencia son una manera de buscar específicamente para la unión temprana.Si está preocupado por
f
obtener un argumento adicional (y, por lo tanto, potencialmente ser llamado erróneamente), hay una forma más sofisticada que implica el uso de un cierre como una "fábrica de funciones":y en su ciclo use en
f = make_f(i)
lugar de ladef
declaración.fuente
La explicación
El problema aquí es que el valor de
i
no se guarda cuandof
se crea la función . Más bien,f
busca el valor dei
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:
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 llamaf
, el valor dei
ha cambiado y se ha establecido en2
.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 predeterminadoA diferencia de las variables de cierre (como
i
), los argumentos predeterminados se evalúan inmediatamente cuando se define la función: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
.Utilice una función de fábrica para capturar el valor actual de
i
en un cierreLa 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 :Úselo
functools.partial
para vincular el valor actual dei
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.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:
¡Observe cómo
i
aún cambió a pesar de que lo convertimos en un argumento predeterminado! Si su código mutai
, entonces debe vincular una copia dei
a su función, así:def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())
fuente