python - ¿Cómo funcionan los cierres léxicos?
closures lazy-evaluation (9)
Así es como lo haces usando la biblioteca de functools
(que no estoy seguro de que estuviera disponible en el momento en que se planteó la pregunta).
from functools import partial
flist = []
def func(i, x): return x * i
for i in xrange(3):
flist.append(partial(func, i))
for f in flist:
print f(2)
Salidas 0 2 4, como se esperaba.
Mientras investigaba un problema que tenía con los cierres léxicos en código JavaScript, llegué a 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 atentamente lambda
. Imprime "4 4 4", lo cual es sorprendente. Esperaría "0 2 4".
Este código equivalente de Perl lo hace bien:
my @flist = ();
foreach my $i (0 .. 2)
{
push(@flist, sub {$i * $_[0]});
}
foreach my $f (@flist)
{
print $f->(2), "/n";
}
"0 2 4" está impreso.
¿Puedes explicar la diferencia?
Actualizar:
El problema no es que i
sea 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
desconozco en ese punto. Aún así, imprime "4 4 4".
El problema es que todas las funciones locales se unen al mismo entorno y, por lo tanto, a la misma variable i
. La solución (solución alternativa) es crear entornos separados (marcos de pila) para cada función (o lambda):
t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]
>>> t[1](2)
2
>>> t[2](2)
4
El razonamiento detrás del comportamiento ya se ha explicado y se han publicado varias soluciones, pero creo que es el más pitónico (¡recuerde que todo en Python es un objeto!):
flist = []
for i in xrange(3):
def func(x): return x * func.i
func.i=i
flist.append(func)
for f in flist:
print f(2)
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 convierte en un argumento "oculto" con un valor predeterminado (funcionará bien, pero no es "pitónico") .
La variable i
es global, cuyo valor es 2 cada vez que se llama a la función f
.
Me inclinaría a implementar el comportamiento que busca de la siguiente manera:
>>> class f:
... def __init__(self, multiplier): self.multiplier = multiplier
... def __call__(self, multiplicand): return self.multiplier*multiplicand
...
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]
Respuesta a su actualización : No es la globalidad de i
per se la que está causando este comportamiento, sino el hecho de que es una variable de un alcance adjunto que tiene un valor fijo sobre los tiempos cuando se llama a f. En su segundo ejemplo, el valor de i
se toma del alcance de la función kkk
, y nada cambia cuando llama a las funciones en flist
.
Las funciones definidas en el ciclo siguen accediendo a la misma variable i
mientras cambia su valor. Al final del ciclo, todas las funciones apuntan a la misma variable, que mantiene el último valor en el ciclo: el efecto es lo que se informó 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 cuando se ejecuta la instrucción def
y, por lo tanto, el valor de la variable de ciclo se congela.
El siguiente funciona como se espera:
flist = []
for i in xrange(3):
def func(x, i=i): # the *value* of i is copied in func() environment
return x * i
flist.append(func)
for f in flist:
print f(2)
Lo que está sucediendo es que la variable i se captura y las funciones devuelven el valor al que está vinculado en el momento en que se lo llama. En los 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 el ejemplo de esquema es hacer con la semántica del ciclo 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 lenguajes. Si usa una variable diferente creada externamente al bucle y la muta, verá el mismo comportamiento en el esquema. Intente reemplazar su ciclo con:
(let ((ii 1)) (
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* ii x)) flist))
(set! ii i))
))
Eche un vistazo here para más discusión sobre esto.
[Editar] Posiblemente una mejor manera de describirlo sea pensar en el ciclo do como una macro que realiza los siguientes pasos:
- Defina una lambda tomando un solo parámetro (i), con un cuerpo definido por el cuerpo del bucle,
- Una llamada inmediata de esa lambda con valores apropiados de i como su parámetro.
es decir. el equivalente a la python a continuación:
flist = []
def loop_body(i): # extract body of the for loop to function
def func(x): return x*i
flist.append(func)
map(loop_body, xrange(3)) # for i in xrange(3): body
El i ya no es el del ámbito principal, sino una nueva variable en su propio ámbito (es decir, el parámetro para el lambda) y así obtienes el comportamiento que observas. Python no tiene este nuevo alcance implícito, por lo que el cuerpo del bucle for solo comparte la variable i.
Python se está comportando 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). Este es exactamente el problema, sin embargo, en este entorno, estoy mutado y todos los cierres se refieren a la misma i .
Esta es la mejor solución que se me ocurre: crear una función Creater e invocarla en su lugar. Esto forzará diferentes entornos para cada una de las funciones creadas, con una i diferente en cada una.
flist = []
for i in xrange(3):
def funcC(j):
def func(x): return x * j
return func
flist.append(funcC(i))
for f in flist:
print f(2)
Esto es lo que sucede cuando mezclas los efectos secundarios y la programación funcional.
Todavía no estoy del todo convencido de por qué en algunos idiomas esto funciona de una manera, y de alguna otra manera. En Common Lisp es como Python:
(defvar *flist* ''())
(dotimes (i 3 t)
(setf *flist*
(cons (lambda (x) (* x i)) *flist*)))
(dolist (f *flist*)
(format t "~a~%" (funcall f 2)))
Imprime "6 6 6" (tenga en cuenta que aquí la lista es de 1 a 3, y está construida en reversa "). Mientras que en Scheme funciona como en Perl:
(define flist ''())
(do ((i 1 (+ 1 i)))
((>= i 4))
(set! flist
(cons (lambda (x) (* i x)) flist)))
(map
(lambda (f)
(printf "~a~%" (f 2)))
flist)
Imprime "6 4 2"
Y como ya he mencionado, Javascript está en el campamento de Python / CL. Parece que aquí hay una decisión de implementación, que los diferentes lenguajes abordan de distintas maneras. Me encantaría entender cuál es la decisión, exactamente.
mira este:
for f in flist:
print f.func_closure
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
Significa que todos apuntan a la misma instancia de variable i, que tendrá un valor de 2 una vez que termine el ciclo.
Una solución legible:
for i in xrange(3):
def ffunc(i):
def func(x): return x * i
return func
flist.append(ffunc(i))