Recientemente comencé a jugar con Python y encontré 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 agregada por un número. Las funciones se construyen en for
bucle donde el iterador i
se extiende desde 0
a 3
. Para cada uno de estos números lambda
, se crea una función que captura i
y agrega a la entrada de la función. La última línea llama a la segunda lambda
función 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 un puntero esencial. Al crear los lambda
cierres para i
, esperaba que almacenara un puntero al objeto entero al que apunta actualmente i
. Eso significa que cuando se le i
asigna un nuevo objeto entero no debería afectar los cierres creados anteriormente. Lamentablemente, la inspección de la adders
matriz dentro de un depurador muestra que sí. Todas las lambda
funciones se refieren al último valor de i
, 3
, que se traduce en adders[1](3)
regresar 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
lambda
funciones de capturar el valor actual dei
una manera que no se verá afectada cuandoi
cambie su valor?
i
deja el espacio de nombres?print i
no funcionaría después del ciclo. Pero lo probé por mí mismo y ahora veo lo que quieres decir: funciona. No tenía idea de que las variables de bucle persistían después del cuerpo del bucle en Python.if
,with
,try
etc.Respuestas:
Su segunda pregunta ha sido respondida, pero en cuanto a la primera:
El alcance en Python es
dinámico yléxico. Un cierre siempre recordará el nombre y el alcance de la variable, no el objeto al que apunta. Como 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 sobre cómo superar esto, hay dos formas que vienen a la mente:
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 para el objeto que desea conservar.
Un poco más detallado pero menos hacky sería crear un nuevo alcance cada vez que cree el lambda:
El alcance aquí se crea usando una nueva función (una 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 tenga una función ordinaria en lugar de la lambda para crear el nuevo ámbito:
fuente
set!
. Vea aquí qué es realmente el alcance dinámico: voidspace.org.uk/python/articles/code_blocks.shtml .Puede forzar la captura de una variable utilizando un argumento con un valor predeterminado:
la idea es declarar un parámetro (ingeniosamente nombrado
i
) y darle un valor predeterminado de la variable que desea capturar (el valor dei
)fuente
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:
fuente
Considere el siguiente código:
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 realiza en un bucle? Sé que 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:
fuente
i
se accede a la misma variable para cada función lambda.En respuesta a su segunda pregunta, la forma más elegante de hacerlo sería utilizar una función que tome dos parámetros en lugar de una matriz:
Sin embargo, usar lambda aquí es un poco tonto. Python nos proporciona el
operator
módulo, que proporciona una interfaz funcional para los operadores básicos. El lambda anterior tiene una sobrecarga innecesaria solo para llamar al operador de adición:Entiendo que estás jugando, tratando de explorar el lenguaje, pero no puedo imaginar una situación en la que usaría una variedad de funciones en las que la rareza de alcance de Python se interpondría en el camino.
Si lo desea, puede escribir una pequeña clase que use su sintaxis de indexación de matriz:
fuente
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.
¿Qué hay en un cierre?
En particular, my_str no está en el cierre de f1.
¿Qué hay en el cierre de f2?
Observe (de las direcciones de memoria) que ambos cierres contienen los mismos objetos. Por lo tanto, puede comenzar a pensar en la función lambda como una referencia al alcance. Sin embargo, my_str no está en el cierre para f_1 o f_2, y yo 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.
¿Los objetos de cierre son el mismo objeto?
fuente
int object at [address X]>
me hizo pensar que el cierre está almacenando [dirección X] AKA una referencia. Sin embargo, [dirección X] cambiará si la variable se reasigna después de la declaración lambda.