¿Cómo se implementan los cierres?

“Aprendiendo Python, 4ª ed.” menciona que:

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

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 sugiere el código de manera intuitiva y define cada función reemplazando i con su valor actual dentro del bucle for cuando el bucle se está ejecutando?

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) tiene tres nombres nuevos: mylist[0], mylist[1], mylist[2] . Eso es lo mismo que sucede cuando 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 Marks sobre las funciones de comstackció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.

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

Esto hace una gran diferencia cuando comstacks una función. Si tiene referencias, conoce la ubicación de la memoria del objeto al que hace referencia. 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 comstack las funciones: donde quiera que haya un nombre en el código, agrega una instrucción que determinará qué significa ese nombre.

Básicamente, Python comstack completamente las funciones, pero los nombres se comstackn 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 comstackdor 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 comstack como una búsqueda en el espacio de nombres de makeActions (porque i usaron 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.

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 escondiendo 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 stack.
  • 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 quita de la stack, ¡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.

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

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

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

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.

     Nivel 1:
         hechos
         yo
     Nivel 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 avanzando hasta el nivel 1 . Como no está ejecutando estas funciones inmediatamente, todas usarán el valor final de i .

¿Expertos?