Mientras investigaba un problema que tenía con los cierres léxicos en el código Javascript, encontré este problema en Python:
flist = []
for i in xrange(3):
def func(x): return x * i
flist.append(func)
for f in flist:
print f(2)
Tenga en cuenta que este ejemplo evita cuidadosamente lambda
. Imprime "4 4 4", lo cual es sorprendente. Yo esperaría "0 2 4".
Este código Perl equivalente lo hace bien:
my @flist = ();
foreach my $i (0 .. 2)
{
push(@flist, sub {$i * $_[0]});
}
foreach my $f (@flist)
{
print $f->(2), "\n";
}
Se imprime "0 2 4".
¿Puedes por favor explicar la diferencia?
Actualizar:
El problema no es con i
ser global. Esto muestra el mismo comportamiento:
flist = []
def outer():
for i in xrange(3):
def inner(x): return x * i
flist.append(inner)
outer()
#~ print i # commented because it causes an error
for f in flist:
print f(2)
Como muestra la línea comentada, i
se desconoce en ese momento. Aún así, imprime "4 4 4".
python
closures
lazy-evaluation
late-binding
Eli Bendersky
fuente
fuente
Respuestas:
Python se está comportando realmente como se define. Se crean tres funciones separadas , pero cada una tiene el cierre del entorno en el que están definidas , en este caso, el entorno global (o el entorno de la función externa si el bucle se coloca dentro de otra función). Sin embargo, este es exactamente el problema: en este entorno, estoy mutado y todos los cierres se refieren al mismo i .
Aquí es la mejor solución que puedo llegar a - crear un creater función e invocar que su lugar. Esto forzará diferentes entornos para cada una de las funciones creadas, con una i diferente en cada una.
Esto es lo que sucede cuando mezclas efectos secundarios y programación funcional.
fuente
def inner(x, i=i): return x * i
Las funciones definidas en el bucle siguen accediendo a la misma variable
i
mientras cambia su valor. Al final del ciclo, todas las funciones apuntan a la misma variable, que contiene el último valor en el ciclo: el efecto es el que se informa en el ejemplo.Para evaluar
i
y usar su valor, un patrón común es establecerlo como un parámetro predeterminado: los valores predeterminados de los parámetros se evalúan cuandodef
se ejecuta la instrucción y, por lo tanto, el valor de la variable de bucle se congela.Lo siguiente funciona como se esperaba:
fuente
def
que se ejecuta la instrucción /i
de la definición. :-(Así es como lo hace usando la
functools
biblioteca (que no estoy seguro de que estuviera disponible en el momento en que se planteó la pregunta).Salidas 0 2 4, como se esperaba.
fuente
functools.partialmethod()
partir de python 3.4mira este:
Significa que todos apuntan a la misma instancia de variable i, que tendrá un valor de 2 una vez que finalice el ciclo.
Una solución legible:
fuente
Lo que sucede es que la variable i se captura y las funciones devuelven el valor al que está vinculada en el momento en que se llama. En lenguajes funcionales, este tipo de situación nunca surge, ya que no sería un rebote. Sin embargo, con python, y también como has visto con lisp, esto ya no es cierto.
La diferencia con su ejemplo de esquema tiene que ver con la semántica del bucle do. Scheme está creando efectivamente una nueva variable i cada vez a través del ciclo, en lugar de reutilizar un enlace i existente como con los otros idiomas. Si usa una variable diferente creada externamente al bucle y la muta, verá el mismo comportamiento en el esquema. Intente reemplazar su bucle con:
Echa un vistazo aquí para más discusión sobre esto.
[Editar] Posiblemente una mejor manera de describirlo es pensar en el bucle do como una macro que realiza los siguientes pasos:
es decir. el equivalente a la siguiente pitón:
El i ya no es el del ámbito principal, sino una variable completamente nueva en su propio ámbito (es decir, el parámetro de la lambda) y así obtiene el comportamiento que observa. Python no tiene este nuevo alcance implícito, por lo que el cuerpo del bucle for solo comparte la variable i.
fuente
Todavía no estoy completamente convencido de por qué en algunos idiomas esto funciona de una manera y de otra. En Common Lisp es como Python:
Imprime "6 6 6" (tenga en cuenta que aquí la lista es del 1 al 3, y está construida al revés "). Mientras que en Scheme funciona como en Perl:
Impresiones "6 4 2"
Y como ya he mencionado, Javascript está en el campo Python / CL. Parece que hay una decisión de implementación aquí, que diferentes lenguajes se acercan de distintas maneras. Me encantaría entender cuál es la decisión, exactamente.
fuente
El problema es que todas las funciones locales se unen al mismo entorno y, por lo tanto, a la misma
i
variable. La solución (solución alternativa) es crear entornos separados (marcos de pila) para cada función (o lambda):fuente
La variable
i
es global, cuyo valor es 2 cada vez quef
se llama a la función .Me inclinaría a implementar el comportamiento que busca de la siguiente manera:
Respuesta a su actualización : No es la globalidad
i
per se lo que está causando este comportamiento, es el hecho de que es una variable de un alcance que tiene un valor fijo en los momentos en que se llama f. En su segundo ejemplo, el valor dei
se toma del alcance de lakkk
función, y nada cambia eso cuando se activan las funcionesflist
.fuente
El razonamiento detrás del comportamiento ya se ha explicado, y se han publicado varias soluciones, pero creo que esta es la más pitónica (recuerde, ¡todo en Python es un objeto!):
La respuesta de Claudiu es bastante buena, usando un generador de funciones, pero la respuesta de piro es un truco, para ser honesto, ya que me está convirtiendo en un argumento "oculto" con un valor predeterminado (funcionará bien, pero no es "pitónico") .
fuente
func
enx * func.i
se referirá siempre a la última función definida. Entonces, aunque cada función individualmente tiene el número correcto pegado, todos terminan leyendo la última de todos modos.