Pregunta

Mientras estaba investigando un problema que tuve 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 conscientemente a lambda . Imprime "4 4 4", lo que es sorprendente. Yo esperaría '' 0 2 4 ''.

Este código Perl equivalente lo hace correctamente:

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * 

Mientras estaba investigando un problema que tuve 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 conscientemente a lambda . Imprime "4 4 4", lo que es sorprendente. Yo esperaría '' 0 2 4 ''.

Este código Perl equivalente lo hace correctamente:

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)

" 0 2 4 " está impreso.

¿Puede explicar la diferencia?


Actualización:

El problema no es con i siendo global. Esto muestra el mismo comportamiento:

<*>

Como se muestra en la línea comentada, i es desconocido en ese momento. Aún así, imprime " 4 4 4 " ;.

[0]}); } foreach my $f (@flist) { print $f->(2), "\n"; }

" 0 2 4 " está impreso.

¿Puede explicar la diferencia?


Actualización:

El problema no es con i siendo global. Esto muestra el mismo comportamiento:

<*>

Como se muestra en la línea comentada, i es desconocido en ese momento. Aún así, imprime " 4 4 4 " ;.

¿Fue útil?

Solución

Python se está comportando como se define. Se crean tres funciones separadas , pero cada una tiene el cierre del entorno en el que se definen , en este caso, el entorno global (o el entorno de la función externa si el el bucle se coloca dentro de otra función). Sin embargo, este es exactamente el problema: en este entorno, i está mutado , y todos los cierres se refieren a la misma i .

Aquí está la mejor solución que se me ocurre: cree una función de creación e invoque eso en su lugar. Esto forzará entornos diferentes para cada una de las funciones creadas, con un diferente i 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 se mezclan los efectos secundarios y la programación funcional.

Otros consejos

Las funciones definidas en el bucle siguen accediendo a la misma variable i mientras su valor cambia. 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 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 bucle se congela.

Lo 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)

Aquí se explica cómo hacerlo utilizando la biblioteca 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.

mira esto:

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 i variable, que tendrá un valor de 2 una vez que finalice el bucle.

Una solución legible:

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))

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 tu ejemplo de esquema es hacer con la semántica del bucle do. Scheme está creando efectivamente una nueva variable i cada vez que pasa por el bucle, en lugar de reutilizar un enlace i existente como con los otros idiomas. Si usa una variable diferente creada externa al bucle y la muta, verá el mismo comportamiento en el esquema. Intenta reemplazar tu 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 a aquí para obtener más informació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 tomando un único parámetro (i), con un cuerpo definido por el cuerpo del bucle,
  2. Una llamada inmediata de esa lambda con valores apropiados de i como su parámetro.

es decir. el equivalente a la siguiente python:

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

La i ya no es la del ámbito principal, sino una nueva variable en su propio ámbito (es decir, el parámetro de la lambda), por lo que 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.

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

Impresiones "6 6 6" (tenga en cuenta que aquí la lista es del 1 al 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 campo Python / CL. Parece que hay una decisión de implementación aquí, que diferentes lenguajes abordan de distintas maneras. Me encantaría entender cuál es la decisión, exactamente.

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

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 lo que está causando este comportamiento, es el hecho de que es una variable de un alcance envolvente que tiene un valor fijo sobre los tiempos en que se llama f. En su segundo ejemplo, el valor de i se toma del alcance de la función kkk , y nada está cambiando eso cuando llama a las funciones en flist .

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top