¿Qué capturan los cierres de funciones lambda?

Resuelto Boaz asked hace 14 años • 8 respuestas

Recientemente comencé a jugar con Python y encontré algo peculiar en la forma en que funcionan los cierres. Considere el siguiente código:

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

Crea una matriz simple de funciones que toman una sola entrada y devuelven esa entrada sumada por un número. Las funciones se construyen en un bucle desde donde se ejecuta forel iterador hasta . Para cada uno de estos números se crea una función que lo captura y lo agrega a la entrada de la función. La última línea llama a la segunda función como parámetro. Para mi sorpresa, el resultado fue .i03lambdailambda36

Esperaba un 4. Mi razonamiento fue: en Python todo es un objeto y, por lo tanto, cada variable es esencial como un puntero a él. Al crear los lambdacierres para i, esperaba que almacenara un puntero al objeto entero al que apunta actualmente i. Eso significa que cuando ise le asigna un nuevo objeto entero, no debería afectar los cierres creados anteriormente. Lamentablemente, inspeccionar la addersmatriz dentro de un depurador muestra que sí. Todas lambdalas funciones hacen referencia al último valor de i, 3lo que da como resultado adders[1](3)el retorno 6.

Lo que me hace preguntarme sobre lo siguiente:

  • ¿Qué capturan exactamente los cierres?
  • ¿Cuál es la forma más elegante de convencer a las lambdafunciones para que capturen el valor actual de iuna manera que no se vea afectada cuando icambie su valor?

Para obtener una versión práctica y más accesible de la pregunta, específica del caso en el que se utiliza un bucle (o comprensión de lista, expresión generadora, etc.), consulte Creación de funciones (o lambdas) en un bucle (o comprensión) . Esta pregunta se centra en comprender el comportamiento subyacente del código en Python.

Si llegó aquí tratando de solucionar un problema al crear botones en Tkinter, intente crear botones con tkinter en un bucle for pasando argumentos de comando para obtener consejos más específicos.

Consulte ¿Qué contiene exactamente un obj.__closure__? para obtener detalles técnicos sobre cómo Python implementa cierres. Consulte ¿Cuál es la diferencia entre enlace temprano y tardío? para discusión de terminología relacionada.

Boaz avatar Feb 19 '10 16:02 Boaz
Aceptado

puedes forzar la captura de una variable usando un argumento con un valor predeterminado:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

la idea es declarar un parámetro (inteligentemente llamado i) y darle un valor predeterminado de la variable que desea capturar (el valor de i)

Adrien Plisson avatar Feb 19 '2010 09:02 Adrien Plisson

¿Qué capturan exactamente los cierres?

Los cierres en Python utilizan alcance léxico: recuerdan el nombre y el alcance de la variable cerrada donde se crea. Sin embargo , siguen siendo de enlace tardío : el nombre se busca cuando se utiliza el código del cierre, no cuando se crea el cierre. Dado que todas las funciones en su ejemplo se crean en el mismo ámbito y usan el mismo nombre de variable, siempre hacen referencia a la misma variable.

En su lugar, existen al menos dos formas de obtener el enlace anticipado:

  1. La forma más concisa, pero no estrictamente equivalente, es la recomendada por Adrien Plisson . Cree una lambda con un argumento adicional y establezca el valor predeterminado del argumento adicional en el objeto que desea conservar.

  2. De manera más detallada pero también más sólida, podemos crear un nuevo alcance para cada lambda creada:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5
    

    El alcance aquí se crea usando una nueva función (otra lambda, para abreviar), que vincula su argumento y pasa el valor que desea vincular como argumento. Sin embargo, en el código real, lo más probable es que tengas una función ordinaria en lugar de lambda para crear el nuevo alcance:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
    
Max Shawabkeh avatar Feb 19 '2010 09:02 Max Shawabkeh

Para completar, otra respuesta a su segunda pregunta: puede usar parcial en el módulo functools .

Al importar agregar desde el operador como propuso Chris Lutz, el ejemplo se convierte en:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
    # store callable object with first argument given as (current) i
    adders[i] = partial(add, i) 

print adders[1](3)
Joma avatar Apr 11 '2012 07:04 Joma

Considere el siguiente código:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Creo que la mayoría de la gente no encontrará esto confuso en absoluto. Es el comportamiento esperado.

Entonces, ¿por qué la gente piensa que sería diferente si se hiciera en bucle? Sé que yo mismo cometí ese error, pero no sé por qué. ¿Es el bucle? ¿O quizás la lambda?

Después de todo, el bucle es sólo una versión más corta de:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a
mthurlin avatar Feb 19 '2010 10:02 mthurlin

Aquí hay un nuevo ejemplo que resalta la estructura de datos y el contenido de un cierre, para ayudar a aclarar cuándo se "guarda" el contexto circundante.

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

¿Qué hay en un cierre?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

En particular, my_str no está en el cierre de f1.

¿Qué hay en el cierre de f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Observe (de las direcciones de memoria) que ambos cierres contienen los mismos objetos. Entonces, puede comenzar a pensar que la función lambda tiene una referencia al alcance. Sin embargo, my_str no está en el cierre de f_1 o f_2, y i no está en el cierre de f_3 (no se muestra), lo que sugiere que los objetos de cierre en sí son objetos distintos.

¿Son los objetos de cierre el mismo objeto?

>>> print f_1.func_closure is f_2.func_closure
False
Jeff avatar May 09 '2014 05:05 Jeff