¿Qué capturan los cierres de función (lambda)?

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

adders=[0,1,2,3] 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 agregada por un número. Las funciones se construyen en el bucle for donde el iterador i ejecuta de 0 a 3 . Para cada uno de estos números se crea una función lambda que captura i y la agrega a la entrada de la función. La última línea llama a la segunda función lambda con 3 como parámetro. Para mi sorpresa la salida fue de 6 .

Esperaba un 4 . Mi razonamiento fue: en Python, todo es un objeto y, por lo tanto, cada variable es un indicador esencial para él. Al crear los cierres lambda para i , esperaba que almacenara un puntero al objeto entero al que actualmente apunta i . Eso significa que cuando asigné un nuevo objeto entero no debería afectar los cierres creados previamente. Lamentablemente, la inspección de la matriz de adders dentro de un depurador muestra que sí lo hace. Todas las funciones lambda refieren al último valor de i , 3 , lo que da como resultado que los adders[1](3) devuelvan 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 funciones lambda de capturar el valor actual de i de una manera que no se verá afectada cuando cambie su valor?

    Su segunda pregunta ha sido respondida, pero en cuanto a su primera:

    ¿Qué captura el cierre exactamente?

    El scope en Python es dynamic y léxico. Un cierre siempre recordará el nombre y el scope de la variable, no el objeto al que apunta. Dado que todas las funciones en su ejemplo se crean en el mismo ámbito y usan el mismo nombre de variable, siempre se refieren a la misma variable.

    EDITAR: En relación con su otra pregunta de cómo superar esto, hay dos maneras que vienen a la mente:

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

    2. Un poco más detallado pero menos complejo sería crear un nuevo ámbito cada vez que crees el lambda:

       >>> 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 ámbito aquí se crea utilizando una nueva función (lambda, por brevedad), que enlaza su argumento, y pasa el valor que desea vincular como argumento. Sin embargo, en el código real, lo más probable es que tenga una función ordinaria en lugar de la lambda para crear el nuevo scope:

       def createAdder(x): return lambda y: y + x adders = [createAdder(i) for i in range(4)] 

    puede forzar la captura de una variable utilizando 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 (con un nombre inteligente i ) y darle un valor predeterminado de la variable que desea capturar (el valor de i )

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

    Con la adición de la importación del 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) 

    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 cuando se hace en un bucle? Sé que yo mismo cometí ese error, pero no sé por qué. Es el bucle? O tal vez la lambda?

    Después de todo, el bucle es solo 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 

    En respuesta a su segunda pregunta, la forma más elegante de hacerlo sería usar una función que tome dos parámetros en lugar de una matriz:

     add = lambda a, b: a + b add(1, 3) 

    Sin embargo, usar lambda aquí es un poco tonto. Python nos proporciona el módulo de operator , que proporciona una interfaz funcional a los operadores básicos. El lambda anterior tiene gastos generales innecesarios solo para llamar al operador de adición:

     from operator import add add(1, 3) 

    Entiendo que estás jugando, tratando de explorar el lenguaje, pero no puedo imaginar una situación en la que usaría una serie de funciones en las que la rareza del scope de Python se interponga.

    Si lo desea, puede escribir una clase pequeña que use su syntax de indexación de matriz:

     class Adders(object): def __getitem__(self, item): return lambda a: a + item adders = Adders() adders[1](3) 

    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 adjunto.

     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 (,) 43 

    Notablemente, 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 (,) 43 

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

    ¿Son los objetos de cierre el mismo objeto?

     >>> print f_1.func_closure is f_2.func_closure False