¿Cómo funcionan los cierres léxicos?

149

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 iser 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, ise desconoce en ese momento. Aún así, imprime "4 4 4".

Eli Bendersky
fuente
3
Aquí hay un artículo bastante bueno sobre este tema. me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

Respuestas:

151

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.

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 efectos secundarios y programación funcional.

Claudiu
fuente
55
Su solución también es la que se usa en Javascript.
Eli Bendersky
9
Esto no es mal comportamiento. Se comporta exactamente como se define.
Alex Coventry
66
IMO piro tiene una mejor solución stackoverflow.com/questions/233673/…
jfs
2
Quizás cambiaría la 'i' más interna por 'j' para mayor claridad.
eggsyntax
77
¿qué pasa simplemente definiendo de esta manera:def inner(x, i=i): return x * i
dashesy
152

Las funciones definidas en el bucle siguen accediendo a la misma variable imientras 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 iy 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 defse ejecuta la instrucción y, por lo tanto, el valor de la variable de bucle se congela.

Lo siguiente funciona como se esperaba:

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)
piro
fuente
77
s / en tiempo de compilación / en el momento en defque se ejecuta la instrucción /
jfs
23
Esta es una solución ingeniosa, lo que lo hace horrible.
Stavros Korokithakis
Hay un problema con esta solución: func tiene ahora dos parámetros. Eso significa que no funciona con una cantidad variable de parámetros. Peor aún, si llama a func con un segundo parámetro, esto sobrescribirá el original ide la definición. :-(
Pascal
34

Así es como lo hace usando la functoolsbiblioteca (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.

Luca Invernizzi
fuente
Realmente quería usar esto, pero mi función es realmente un método de clase y el primer valor pasado es self. ¿Hay alguna forma de evitar esto?
Michael David Watson el
1
Absolutamente. Suponga que tiene una clase Math con un método add (self, a, b) y desea establecer a = 1 para crear el método 'incremental'. Luego, cree una instancia de su clase 'my_math', y su método de incremento será 'increment = partial (my_math.add, 1)'.
Luca Invernizzi
2
Para aplicar esta técnica a un método que también podría usar a functools.partialmethod()partir de python 3.4
Matt Eding
13

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 finalice 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))
Nulo303
fuente
1
Mi pregunta es más "general". ¿Por qué Python tiene este defecto? Esperaría que un lenguaje que respalde los cierres léxicos (como Perl y toda la dinastía Lisp) resuelva esto correctamente.
Eli Bendersky
2
Preguntar por qué algo tiene un defecto es asumir que no es un defecto.
Null303
7

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:

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

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:

  1. Defina una lambda que tome un solo parámetro (i), con un cuerpo definido por el cuerpo del bucle,
  2. Una llamada inmediata de esa lambda con los valores apropiados de i como parámetro.

es decir. el equivalente a la siguiente pitó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 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.

Brian
fuente
Interesante. No sabía la diferencia en la semántica del bucle do. Gracias
Eli Bendersky
4

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:

(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 del 1 al 3, y está construida al revés "). 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)

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.

Eli Bendersky
fuente
8
La diferencia está en las (hacer ...) en lugar de las reglas de alcance. En el esquema do crea una nueva variable cada paso a través del ciclo, mientras que otros lenguajes reutilizan el enlace existente. Vea mi respuesta para obtener más detalles y un ejemplo de una versión de esquema con un comportamiento similar al lisp / python.
Brian
2

El problema es que todas las funciones locales se unen al mismo entorno y, por lo tanto, a la misma ivariable. 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
Rafał Dowgird
fuente
1

La variable ies global, cuyo valor es 2 cada vez que fse llama a la función .

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 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 de ise toma del alcance de la kkkfunción, y nada cambia eso cuando se activan las funciones flist.

Alex Coventry
fuente
0

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!):

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 está convirtiendo en un argumento "oculto" con un valor predeterminado (funcionará bien, pero no es "pitónico") .

darkfeline
fuente
Creo que depende de tu versión de Python. Ahora tengo más experiencia y ya no sugeriría esta forma de hacerlo. Claudiu's es la forma correcta de hacer un cierre en Python.
darkfeline
1
Esto no funcionará en Python 2 o 3 (ambos generan "4 4 4"). El funcen x * func.ise 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.
Lambda Fairy