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
itarde posible (por lo tanto, cuando se llama después del final del ciclo,ise 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
ieni=iun valor por defecto para el nombre de argumentoi, que es el de la izquierdaieni=i) se buscan endefel tiempo, no en elcalltiempo, lo que en esencia son una manera de buscar específicamente para la unión temprana.Si está preocupado por
fobtener 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 ladefdeclaración.fuente
La explicación
El problema aquí es que el valor de
ino se guarda cuandofse crea la función . Más bien,fbusca el valor deicuá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_varha 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 deiha 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
iusá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
ien un cierreLa 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 :Úselo
functools.partialpara vincular el valor actual deiaffunctools.partialle 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
iaún cambió a pesar de que lo convertimos en un argumento predeterminado! Si su código mutai, entonces debe vincular una copia deia su función, así:def f(i=i.copy()):f = f_factory(i.copy())f_with_i = functools.partial(f, i.copy())fuente