2012-06-19 20 views
13

Tengo un simple código siguiente:comportamiento extraño en el cierre pitón

def get(): 
    return [lambda: i for i in [1, 2, 3]] 

for f in get(): 
    print(f()) 

como se espera de mi conocimiento pitón, la salida es 3 - lista completa contendrá último valor de i. ¿Pero cómo funciona esto internamente?

AFAIK, las variables de python son simplemente referencias a los objetos, por lo que el primer cierre debe incluir la primera referencia del objeto i - y este objeto es definitivamente 1, no 3 O_O. ¿Cómo sucede que el cierre de python incluye la variable en sí misma en lugar de objeto esta referencia de variable? ¿Guarda el nombre de la variable como texto sin formato, alguna "referencia a la variable" o qué?

Respuesta

9

Los cierres no se refieren a variables sino a alcances. Como el último valor de i en su alcance es '3', los tres cierres devuelven lo mismo. Para "bloquear" el valor actual de una variable, crear un nuevo ámbito sólo para él:

def get() : return [ (lambda x: lambda: x)(i) for i in [ 1, 2, 3 ] ] 
for f in get() : print(f()) 
+0

¿Hay alguna información adicional disponible sobre el tema para que pueda ver qué variables ocultas se usan para "guardar" el alcance, acceder a él, etc.? – grigoryvp

+2

'(lambda x: lambda: x) (i)' es una de las cosas más feas que he visto hacer en Python. Ick. (No es que su respuesta sea incorrecta o mala, per se, solo dice: es difícil de leer). –

+2

@EyeofHell: Creo que [pep-227] (http://www.python.org/dev/peps/pep-0227/) es el documento canónico sobre las reglas de alcance de Python. Además, hay algunas buenas respuestas aquí en SO, p. [aquí] (http://stackoverflow.com/questions/291978/short-description-of-python-scoping-rules) – georg

11

Como @ thg435 señala, una lambda no encapsular los valores en ese momento, sino más bien el alcance. Hay demasiado pequeñas maneras en que puede hacer frente a esto:

lambda argumento predeterminado "piratear"

[ lambda v=i: v for i in [ 1, 2, 3 ] ] 

O utilizar functools.partial

from functools import partial 
[ partial(lambda v: v, i) for i in [ 1, 2, 3 ] ] 

Esencialmente, usted tiene que mover el alcance a ser local la función que estás creando En general, me gusta usar partial más a menudo ya que puede pasarle un invocable, y cualquier args y kargs para crear un invocable con un cierre apropiado. Internamente, está ajustando su original invocable para que el alcance se cambie para usted.

+5

+1 Por usar parcial aquí, creo que es un enfoque mucho más limpio. Sin embargo, es posible que desee editar su código, los espacios dentro de los corchetes de la lista son malos de acuerdo con PEP-8. (Sé que estabas siguiendo al asker, lo edité allí). –

+0

@Lattyware: ¡Gracias! Las lambdas son geniales, pero siempre las encuentro más desordenadas para leer. parcial solo se siente mucho más legible y portátil. – jdi

+0

100% de acuerdo, trato de evitar lambdas siempre que puedo, y este es un caso principal en el que el parcial es a la vez más claro y simple. –

4

Cada lambda se está refiriendo a la misma i, que es una variable creada por la lista de comprensión. Al finalizar la comprensión de la lista, i mantiene el valor del elemento final que se le asignó hasta que salga del alcance (lo cual se evita encapsulándolo dentro de una función y devolviéndola, es decir, lambda). Como otros han señalado, los cierres no mantienen copias de los valores, sino que mantienen referencias a las variables que se definieron dentro de su alcance.