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


Recientemente empecé a jugar con Python y me encontré con 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)

Construye una matriz simple de funciones que toman una sola entrada y devuelven esa entrada añadida por un número. Las funciones se construyen en for bucle donde el iterador i ejecuta desde 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 la segunda función lambda con 3 como parámetro. Para mi sorpresa la salida fue 6.

Esperaba un 4. Mi razonamiento fue: en Python todo es un objeto y por lo tanto cada variable es esencial un puntero a é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 i asigna un nuevo objeto integer no debería afectar los cierres creados previamente. Por desgracia, la inspección de la adders array dentro de un depurador muestra que lo hace. Todas las funciones lambda se refieren al último valor de i, 3, lo que resulta en adders[1](3) volviendo 6.

Que me hacen preguntarme sobre lo siguiente: {[25]]}

  • ¿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 i cambie su valor?
Author: martineau, 2010-02-19

6 answers

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

¿Qué captura exactamente el cierre?

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

EDITAR: Con respecto a 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 una 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 hackeado sería crear un nuevo ámbito cada vez que cree la 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 usando una nueva función (a lambda, por brevedad), que enlaza su argumento, y pasa el valor que desea enlazar como argumento. En código real, sin embargo, lo más probable es que tenga una función ordinaria en lugar de la lambda para crear el nuevo ámbito:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
    
 124
Author: Max Shawabkeh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:31:31

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

 151
Author: Adrien Plisson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-19 09:59:22

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

Con la importación de agregar desde el operador como Chris Lutz propuso 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)
 24
Author: Joma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-11 07:11:57

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 se por qué. Es el bucle? ¿O quizás el 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
 16
Author: truppo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-19 10:45:54

En respuesta a su segunda pregunta, la forma más elegante de hacer esto sería usar una función que toma 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 operator, que proporciona una interfaz funcional para los operadores básicos. La lambda anterior tiene sobrecarga innecesaria 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 un situación Usaría una serie de funciones donde la rareza del alcance de Python se interpondría en el camino.

Si lo desea, podría escribir una clase pequeña que use su sintaxis de indexación de matrices:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)
 3
Author: Chris Lutz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-19 09:55:40

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 de cierre."

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 

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
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

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

¿Son los objetos de cierre mismos el mismo objeto?

>>> print f_1.func_closure is f_2.func_closure
False
 3
Author: Jeff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-09 05:19:18