sintaxis - lambda python 3 español
¿Qué capturan los cierres de función(lambda)? (6)
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
(<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 (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 alcance. 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
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 era: 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 hace 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 dei
de una manera que no se verá afectada cuando cambie su valor?
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 alcance de Python se interponga.
Si lo desea, puede escribir una clase pequeña que use su sintaxis de indexación de matriz:
class Adders(object):
def __getitem__(self, item):
return lambda a: a + item
adders = Adders()
adders[1](3)
Para completar otra respuesta a su segunda pregunta: puede usar partial en el módulo functools .
Con la adición de importación 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)
Su segunda pregunta ha sido respondida, pero en cuanto a su primera:
¿Qué captura el cierre exactamente?
El alcance 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 en 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:
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.
Un poco más detallado pero menos intrépido 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 el 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 ámbito:
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
)