tutorial decorators python closures

python - decorators - ¿Cómo se implementan los cierres?



decorators python tutorial (5)

"Aprendiendo Python, 4ta ed." menciona que:

la variable de ámbito adjunto se busca cuando las funciones anidadas se llaman más adelante.

Sin embargo, pensé que cuando una función sale, todas sus referencias locales desaparecen.

def makeActions(): acts = [] for i in range(5): # Tries to remember each i acts.append(lambda x: i ** x) # All remember same last i! return acts

makeActions()[n] es la misma para cada n porque la variable i se busca de alguna manera en el tiempo de llamada. ¿Cómo busca Python esta variable? ¿No debería no existir en absoluto porque ya ha salido de makeActions? ¿Por qué Python no hace lo que el código sugiere de manera intuitiva y define cada función reemplazando i con su valor actual dentro del bucle for cuando el bucle se está ejecutando?

Del comentario a THC4k:
Creo que me equivoqué en la forma en que Python construye funciones en la memoria. Estaba pensando que al encontrar una def o un lambda , Python generaría todas las instrucciones de máquina necesarias relacionadas con esa función y las guardaría en algún lugar de la memoria. Ahora creo que Python guarda más la función como una cadena de texto (y la agrupa con las referencias necesarias para el cierre) y la vuelve a analizar cada vez que se llama a la función.


Pensé que cuando una función sale, todas sus referencias locales desaparecen.

Excepto para los locales que están cerrados en un cierre. Esos no desaparecen, incluso cuando la función a la que son locales ha regresado.


Creo que es bastante obvio lo que sucede cuando piensas que i es un nombre, no algún tipo de valor . Tu función lambda hace algo así como "toma x: busca el valor de i, calcula i ** x" ... así que cuando realmente ejecutas la función, mira hacia arriba i justo en ese momento i es 4 .

También puede usar el número actual, pero tiene que hacer que Python lo vincule a otro nombre:

def makeActions(): def make_lambda( j ): return lambda x: j * x # the j here is still a name, but now it wont change anymore acts = [] for i in range(5): # now you''re pushing the current i as a value to another scope and # bind it there, under a new name acts.append(make_lambda(i)) return acts

Puede parecer confuso, porque a menudo le enseñan que una variable y su valor son lo mismo, lo cual es cierto, pero solo en idiomas que usan variables. Python no tiene variables, sino nombres.

Sobre tu comentario, en realidad puedo ilustrar el punto un poco mejor:

i = 5 myList = [i, i, i] i = 6 print(myList) # myList is still [5, 5, 5].

Usted dijo que cambió i a 6 , eso no es lo que realmente sucedió: i=6 significa "tengo un valor, 6 y quiero nombrarlo i ". El hecho de que ya haya usado i como nombre no tiene importancia para Python, simplemente reasignará el nombre , no cambiará su valor (que solo funciona con variables).

Se podría decir que en myList = [i, i, i] , cualquier valor que señale actualmente (el número 5) obtiene tres nombres nuevos: mylist[0], mylist[1], mylist[2] . Eso es lo mismo que sucede cuando se llama a una función: los argumentos reciben nombres nuevos. Pero eso probablemente va en contra de cualquier intuición acerca de las listas ...

Esto puede explicar el comportamiento en el ejemplo: Usted asigna mylist[0]=5 , mylist[1]=5 , mylist[2]=5 - no es de extrañar que no cambien cuando reasigne la i . Si i fuera algo mutable, por ejemplo, una lista, al cambiarlo también se reflejaría en todas las entradas en myList , ¡porque solo tienes nombres diferentes para el mismo valor !

El simple hecho de que puede usar mylist[0] en la mano izquierda de a = demuestra que es un nombre. Me gusta llamar = el operador asignar nombre : toma un nombre a la izquierda y una expresión a la derecha, luego evalúa la expresión (función de llamada, busca los valores detrás de los nombres) hasta que tenga un valor y finalmente le dé el nombre al valor. No cambia nada .

Para el comentario de Marcos sobre las funciones de compilación:

Bueno, las referencias (y los punteros) solo tienen sentido cuando tenemos algún tipo de memoria direccionable. Los valores se almacenan en algún lugar de la memoria y las referencias lo llevan a ese lugar. Usar una referencia significa ir a ese lugar en la memoria y hacer algo con él. El problema es que Python no utiliza ninguno de estos conceptos.

Python VM no tiene ningún concepto de memoria: los valores flotan en algún lugar del espacio y los nombres son pequeñas etiquetas conectadas a ellos (por una pequeña cadena roja). ¡Los nombres y valores existen en mundos separados!

Esto hace una gran diferencia cuando compilas una función. Si tiene referencias, conoce la ubicación de la memoria del objeto al que se refiere. Entonces simplemente puede reemplazar y luego hacer referencia a esta ubicación. Los nombres de la otra parte no tienen ubicación, por lo que lo que tienes que hacer (durante el tiempo de ejecución) es seguir esa pequeña cadena roja y usar lo que esté en el otro extremo. Esa es la forma en que Python compila las funciones: siempre que haya un nombre en el código, agrega una instrucción que determinará qué significa ese nombre.

Básicamente, Python compila completamente las funciones, pero los nombres se compilan como búsquedas en los espacios de nombres de anidamiento, no como algún tipo de referencia a la memoria.

Cuando usas un nombre, el compilador de Python intentará averiguar a qué espacio de nombre pertenece. Esto da como resultado una instrucción para cargar ese nombre desde el espacio de nombres que encontró.

Lo que lo regresa a su problema original: en lambda x:x**i , i se compila como una búsqueda en el espacio de nombres de makeActions (porque i usé allí). Python no tiene ni idea, ni se preocupa por el valor detrás de él (ni siquiera tiene que ser un nombre válido). Una vez que se ejecuta el código i se busca en su espacio de nombres original y da el valor más o menos esperado.


Intuitivamente, uno podría pensar que sería capturado en su estado actual, pero ese no es el caso. Piense en cada capa como un diccionario de pares de valores de nombre.

Level 1: acts i Level 2: x

Cada vez que crea un cierre para el lambda interno está capturando una referencia al nivel uno. Solo puedo asumir que el tiempo de ejecución realizará una búsqueda de la variable i , comenzando en el nivel 2 y abriéndose paso hasta el nivel 1 . Como no está ejecutando estas funciones inmediatamente, todas usarán el valor final de i .

¿Expertos?


Las referencias locales persisten porque están contenidas en el ámbito local, al que el cierre mantiene una referencia.


Qué pasa cuando creas un cierre:

  • El cierre se construye con un puntero al marco (o aproximadamente, el bloque ) en el que se creó: en este caso, el bloque for .
  • El cierre realmente asume la propiedad compartida de ese marco, incrementando el recuento de ref del marco y ocultando el puntero a ese marco en el cierre. Ese marco, a su vez, mantiene las referencias a los marcos en los que estaba encerrado, para las variables que fueron capturadas más arriba en la pila.
  • El valor de i en ese marco sigue cambiando mientras se ejecuta el bucle for: cada asignación a i actualiza el enlace de i en ese marco.
  • Una vez que el bucle for sale, el cuadro se extrae de la pila, ¡pero no se tira como suele ser! En su lugar, se mantiene porque la referencia del cierre al marco todavía está activa. En este punto, sin embargo, el valor de i ya no se actualiza.
  • Cuando se invoca el cierre, recoge cualquier valor de i esté en el marco principal en el momento de la invocación. Ya que en el bucle for usted crea cierres, pero en realidad no los invoca , el valor de i en la invocación será el último valor que tenía después de que se realizó todo el bucle.
  • Las futuras llamadas a makeActions crearán diferentes marcos. No reutilizará el marco anterior del bucle for, ni actualizará el valor i ese marco anterior, en ese caso.

En resumen: los marcos se recolectan como elementos de Python, y en este caso, se mantiene una referencia adicional al marco correspondiente al bloque for para que no se destruya cuando el bucle for sale del ámbito.

Para obtener el efecto que desea, debe tener un nuevo marco creado para cada valor de i que desee capturar, y cada lambda debe crearse con una referencia a ese nuevo marco. No obtendrá eso del bloque for , pero podría obtenerlo de una llamada a una función auxiliar que establecerá el nuevo marco. Vea la respuesta de THC4k para una posible solución en este sentido.