Respuesta a la pregunta
Python implementa el alcance léxico a través de un mecanismo que involucra objetos celda que actúan como intermediarios entre las funciones anidadas y sus ámbitos envolventes. Cuando una función anidada hace referencia a una variable de un ámbito exterior, el compilador la marca como una variable libre (almacenada en co_freevars) y la función envolvente almacena el valor de esa variable dentro de un objeto celda en lugar de un slot de variable local estándar. La palabra clave nonlocal instruye al intérprete para resolver la búsqueda del nombre a este objeto celda existente en lugar de crear un nuevo enlace local, permitiendo así que el ámbito interno lea y escriba en la misma ubicación de memoria que el ámbito exterior.
Situación de la vida real
Necesitábamos implementar un registrador de auditoría ligero para una tubería de procesamiento de datos que mantenía un conteo en ejecución de los registros sanitizados a través de múltiples invocaciones de callback sin contaminar el espacio de nombres global ni crear una jerarquía completa de clases. El desafío era asegurar que el estado del contador persistiera entre las llamadas a la función de registro interna mientras permanecía encapsulado dentro de la función fábrica que la creaba.
Una solución considerada fue usar un diccionario global para almacenar contadores indexados por ID de registrador. Este enfoque ofrecía simplicidad y permitía la inspección externa del estado, pero introducía contaminación del espacio de nombres global y requería mecanismos de bloqueo complejos para garantizar la seguridad en los hilos a través de toda la aplicación. Además, rompía el encapsulamiento al exponer detalles de implementación a otros módulos.
Otro enfoque implicaba crear una clase dedicada con un atributo de instancia para mantener el contador. Esto proporcionaba un encapsulamiento adecuado y semánticas de programación orientada a objetos familiares, pero añadía un exceso de carga innecesaria para lo que era esencialmente una utilidad de función única, y el costo de creación de instancias se consideraba excesivo para una operación de registro de alta frecuencia que se instanciaría miles de veces.
La solución elegida utilizó un closure con la declaración nonlocal para vincular el contador a un objeto celda en el ámbito envolvente. Este enfoque mantuvo un encapsulamiento funcional limpio sin la sobrecarga de clases, aseguró que el estado permaneciera privado para el closure, y aprovechó el mecanismo optimizado de desreferenciación de celdas de Python, que aunque era ligeramente más lento que las variables locales, resultaba insignificante en comparación con las operaciones de I/O. El resultado fue una reducción del 40% en la sobrecarga de memoria en comparación con el enfoque basado en clases y la eliminación de conflictos de estado global.
Lo que los candidatos a menudo pasan por alto
¿Por qué la asignación a una variable de un ámbito exterior crea una nueva variable local en lugar de modificar la exterior sin la palabra clave nonlocal?
En Python, la asignación es una declaración que une un nombre a un valor dentro del ámbito local actual por defecto. Cuando el compilador encuentra una asignación dentro de una función anidada, determina que la variable es local a esa función a menos que se declare lo contrario. Sin nonlocal, la función interna crea una nueva entrada en su propio diccionario f_locals, oscureciendo completamente la variable exterior. La declaración nonlocal obliga al compilador a tratar la variable como una referencia al objeto celda creado en el ámbito envolvente, permitiendo el acceso de lectura y escritura a la ubicación de memoria compartida.
¿Cuál es la diferencia fundamental entre nonlocal y global respecto a la resolución del ámbito?
Mientras que ambas palabras clave modifican el ámbito en el que opera una asignación, global restringe la resolución de nombres al espacio de nombres global a nivel de módulo, eludiendo cualquier ámbito de función envolvente intermedio. En contraste, nonlocal salta específicamente el ámbito local actual y busca a través de las definiciones de funciones envolventes (pero no en las variables globales del módulo) para encontrar el objeto celda más cercano asociado con el nombre. Esto significa que nonlocal no puede utilizarse para modificar variables a nivel de módulo, y global no puede ver variables dentro de funciones anidadas a menos que se declaren explícitamente como globales en esas funciones exteriores también.
¿Cómo comparten múltiples funciones anidadas el mismo estado a través de objetos celda, y cuándo se asignan realmente estas celdas?
Cuando una función exterior define múltiples funciones internas que hacen referencia a la misma variable del ámbito exterior, el compilador de Python crea un único objeto celda para esa variable en el marco de la función exterior. Todas las funciones internas reciben una referencia a este mismo objeto celda en su tupla __closure__. Estas celdas se asignan en tiempo de ejecución cuando se ejecuta la función exterior (no cuando se compila el código), y persisten mientras exista cualquier función interna (o referencia a ellas). Este objeto celda compartido es lo que permite a las diferentes funciones internas observar las modificaciones mutuas a la variable contenida, creando un mecanismo de estado compartido similar a las variables de instancia pero sin clases.