El Modelo de Ejecución de Python
Todas las demás páginas de esta sección (variables, flujo de control, funciones, módulos) se basan en un modelo subyacente de cómo Python ejecuta realmente el código.
Ese modelo es más simple de lo que parece desde fuera: los nombres se enlazan a objetos dentro de espacios de nombres, y casi todo lo que haces en Python es alguna variación de crear un objeto y enlazarle un nombre.
Esta página es el modelo mental subyacente a Variables, Tipos y Tipado Dinámico, Flujo de Control, Funciones, Argumentos y Alcance y Módulos, Importaciones y Paquetes.
Una vez que lo entiendes, las reglas de alcance, el almacenamiento en caché de importaciones y la trampa de los argumentos predeterminados mutables dejan de parecer peculiaridades separadas y empiezan a parecer el mismo mecanismo que aparece en diferentes lugares.
Resumen
- La ejecución del código Python es el acto repetido de enlazar nombres a objetos dentro de espacios de nombres: la asignación,
def,classeimportson todas especializaciones de esta única operación. - Por Qué Importa: Una vez que ves los nombres y los objetos como cosas separadas conectadas por enlaces, las reglas de alcance, el estado mutable compartido y el almacenamiento en caché de importaciones dejan de ser casos especiales y se convierten en consecuencias predecibles de un único mecanismo.
- Conceptos Clave: enlace de nombres, espacio de nombres, alcance (LEGB), identidad de objeto, almacenamiento en caché de módulos.
- Cuándo Usar: Para razonar sobre por qué dos variables se mutan juntas, por qué una función lee una variable externa pero no puede reasignarla sin
nonlocal, o por qué editar un archivo de módulo a mitad de la ejecución no afecta a una copia ya importada. - Limitaciones / Compensaciones: La flexibilidad del enlace de nombres dinámico tiene un costo en garantías estáticas: el tipo de destino de un nombre puede cambiar en tiempo de ejecución, y las herramientas solo pueden aproximar lo que un lenguaje totalmente estático verifica de forma gratuita.
- Temas Relacionados: semántica de referencia y mutabilidad, alcance de funciones y cierres (closures), el sistema de importación, el modelo de objetos de CPython.
Fundamentos
El cambio de mentalidad más útil sobre Python es dejar de decir que x = 5 es "almacenar 5 en x" y empezar a decir que es "enlazar el nombre x a un objeto entero 5".
El objeto existe de forma independiente, con su propio tipo, valor e identidad; el nombre es solo una etiqueta que apunta a él, almacenada en un espacio de nombres.
Un espacio de nombres es, concretamente, un diccionario que mapea nombres (cadenas de texto) a objetos: globals() y locals() te dan una visión directa de dos de ellos.
x = [1, 2, 3]
print(globals()["x"] is x) # True - el espacio de nombres del módulo es realmente un dictLa asignación (x = ...), def, class e import se reducen a la misma primitiva: construir (o localizar) un objeto, y luego enlazarle un nombre en el espacio de nombres actual.
Por eso una función y un módulo tienen atributos que puedes inspeccionar: un objeto de función tiene __name__, __doc__, __defaults__; un objeto de módulo tiene los nombres que su código de nivel superior enlazó.
Una analogía útil es un sistema de archivo: los objetos son las carpetas físicas que están en almacenamiento, y los nombres son notas adhesivas que apuntan a ellas; mover una nota adhesiva (reasignar un nombre) nunca toca la carpeta, pero escribir dentro de una carpeta (mutar un objeto) es visible para todas las notas adhesivas que apuntan a ella.
Mecánica e Interacciones
Cada espacio de nombres existe dentro de un alcance específico, y Python resuelve un nombre desnudo buscando en los alcances en un orden fijo: Local, Envolvente (Enclosing), Global, Incorporado (Built-in) - LEGB.
Cada llamada a una función crea un nuevo espacio de nombres local (un frame), que es por lo que las llamadas recursivas no sobrescriben las variables de las otras, aunque compartan el mismo objeto de código.
nonlocal y global existen porque leer un nombre busca hacia afuera a través de LEGB automáticamente, pero asignar a un nombre por defecto lo crea localmente; esas dos palabras clave le dicen a Python "salta la creación local, reasigna el nombre que ya existe más afuera".
def make_counter():
count = 0
def increment():
nonlocal count # sin esto, count += 1 lanza UnboundLocalError
count += 1
return count
return increment
tick = make_counter()
print(tick(), tick()) # 1 2 - el estado vive en el frame envolvente, no globalmenteEl sistema de import superpone un mecanismo de almacenamiento en caché sobre el mismo modelo: el primer import mymodule ejecuta el código de nivel superior de ese archivo exactamente una vez, construye un objeto de módulo a partir del espacio de nombres resultante y lo almacena en sys.modules; cualquier import mymodule posterior en cualquier lugar del proceso simplemente busca ese objeto almacenado en caché en lugar de volver a ejecutar el archivo.
Este único hecho explica dos sorpresas comunes a la vez: editar el código fuente de un módulo a mitad del proceso no tiene efecto hasta que el proceso se reinicia o el módulo se recarga explícitamente, e importar un módulo con efectos secundarios (como abrir una conexión en el nivel superior) solo incurre en ese costo una vez, sin importar cuántos archivos lo importen.
Los valores de argumento predeterminados interactúan con este modelo de una manera que frustra a casi todos una vez: def f(items=[]) evalúa [] una sola vez, cuando se ejecuta la declaración def, y almacena ese único objeto de lista en __defaults__ de la función; cada llamada que omite items comparte ese mismo objeto, por lo que mutarlo dentro de la función filtra el estado entre llamadas.
Consideraciones Avanzadas y Aplicaciones
CPython compila el código fuente en bytecode (visible a través del módulo dis) antes de que una máquina virtual lo ejecute frame por frame, que es por lo que Python se llama a menudo un lenguaje interpretado, aunque exista un paso de compilación real.
Durante la mayor parte de la historia de CPython, el Global Interpreter Lock (GIL) ha serializado la ejecución de bytecode entre hilos en un proceso, lo que significa que el código multihilo con uso intensivo de CPU rara vez se ejecuta más rápido que el código de un solo hilo, una consecuencia directa de cómo el modelo de objetos de conteo de referencias gestiona la seguridad de la memoria.
Python 3.13 introdujo una compilación free-threaded experimental (PEP 703) que puede ejecutarse sin el GIL, y Python 3.14 continúa estabilizándola como un modo de compilación oficial (aunque todavía opcional); una evolución genuina de este modelo de ejecución, no solo un ajuste de configuración.
| Modelo de ejecución | Fortaleza | Debilidad | Mejor ajuste |
|---|---|---|---|
| Bytecode CPython + GIL (compilación predeterminada) | Conteo de referencias simple y seguro; compatibilidad con un gran ecosistema | Un hilo ejecuta bytecode de Python a la vez por proceso | Scripts de propósito general, servicios con uso intensivo de E/S, la mayoría del código de producción actual |
| Compilación free-threaded de CPython (3.13+, opcional) | Paralelismo real multinúcleo para trabajo de CPU puro en Python | Más nuevo, alguna incompatibilidad con extensiones C, sobrecarga adicional de bloqueo por objeto | Cargas de trabajo con uso intensivo de CPU que no pueden usar multiprocesamiento o código nativo |
| Aislamiento de multiprocesamiento / subprocesos | Evita el GIL por completo a través de procesos separados | Mayor uso de memoria, comunicación entre procesos más lenta | Trabajo con uso intensivo de CPU hoy en día, en cualquier compilación de Python compatible |
La recolección de basura sigue la misma historia de objetos y referencias: CPython utiliza principalmente el conteo de referencias (un objeto se libera en el instante en que su recuento llega a cero), respaldado por un recolector cíclico que barre periódicamente los ciclos de referencias que el conteo por sí solo no puede capturar, como dos objetos que se apuntan mutuamente.
Entender los espacios de nombres como diccionarios también explica por qué dir(), vars() y getattr() funcionan de manera uniforme en módulos, instancias y clases; todos ellos simplemente inspeccionan o consultan un objeto respaldado por un espacio de nombres en lugar de tres características de lenguaje no relacionadas.
Conceptos erróneos comunes
- "Las variables de Python son como las variables de C: ranuras de almacenamiento tipadas." Un nombre no tiene tipo propio; el objeto al que está enlazado tiene un tipo, y el mismo nombre puede ser re-enlazado a un tipo de objeto completamente diferente en cualquier momento.
- "La asignación copia el valor." La asignación enlaza un nombre al objeto existente; dos nombres pueden apuntar al mismo objeto mutable, por lo que
y = x; y.append(1)también cambia lo quexve para objetos mutables. - "Cada
importvuelve a ejecutar el código del módulo." Solo la primera importación por proceso lo hace; cada importación subsiguiente del mismo nombre de módulo devuelve el objeto en caché desys.modules. - "Una función anidada puede leer y escribir libremente cualquier variable envolvente." La lectura recorre LEGB automáticamente, pero la asignación dentro de una función anidada crea un nuevo nombre local por defecto a menos que
nonlocalindique lo contrario. - "Los argumentos predeterminados mutables se re-evalúan en cada llamada." La expresión predeterminada se ejecuta una vez, en el momento de
def, y el objeto resultante se reutiliza en cada llamada que omite ese argumento.
Preguntas Frecuentes
¿Qué significa realmente decir que un nombre está "enlazado" a un objeto?
Significa que ahora existe una entrada que mapea ese nombre (una cadena de texto) a ese objeto en algún espacio de nombres: una estructura similar a un diccionario, como los globales de un módulo, los locales de una función o el cuerpo de una clase.
¿Por qué `x = y` no hace que `x` sea una copia de `y`?
Porque la asignación nunca copia datos; solo crea o actualiza un enlace nombre-objeto, por lo que x e y terminan apuntando al mismo objeto cuando el lado derecho ya es una referencia a un objeto.
¿Qué es exactamente LEGB?
Es el orden fijo que Python busca al resolver un nombre desnudo: Local (el frame de la función actual), Envolvente (cualquier frame de función exterior), Global (el espacio de nombres del módulo) y Incorporado (nombres incorporados de Python como len o print).
¿Por qué asignar a una variable dentro de una función a veces lanza `UnboundLocalError`?
Si el cuerpo de la función asigna a ese nombre en algún lugar, Python lo trata como local para toda la función, por lo que leerlo antes de esa asignación falla; se necesitan global o nonlocal para reasignar un nombre externo en lugar de sombrearlo.
¿Es un módulo de Python básicamente un diccionario?
Funcionalmente, sí: el espacio de nombres de un objeto de módulo (todo lo que puedes acceder como module.name) está respaldado por un diccionario que se rellena ejecutando el código de nivel superior del módulo una vez.
¿Qué sucede si importo el mismo módulo desde dos archivos diferentes?
Python ejecuta el código de nivel superior del módulo solo la primera vez; cada importación subsiguiente en cualquier lugar del mismo proceso recupera el objeto de módulo en caché idéntico de sys.modules en lugar de re-ejecutar nada.
¿Por qué la trampa de los argumentos predeterminados mutables está conectada a este modelo de ejecución?
Porque una declaración def ejecuta sus expresiones de valor predeterminado exactamente una vez y almacena el objeto resultante en la función; cada llamada que omite ese argumento reutiliza el mismo objeto, por lo que mutarlo acumula estado entre llamadas.
¿Qué es un espacio de nombres, concretamente?
Un mapeo de nombres a objetos; en la práctica, la mayoría de los espacios de nombres (globales de módulos, locales de funciones, cuerpos de clases) se implementan como diccionarios que puedes inspeccionar con globals(), locals() o vars().
¿Compila Python código antes de ejecutarlo?
Sí, CPython compila primero el código fuente en bytecode, luego una máquina virtual ejecuta ese bytecode frame por frame; por eso se describe comúnmente como interpretado, aunque ocurra un paso de compilación real.
¿Por qué solo un hilo puede ejecutar bytecode de Python a la vez en un proceso CPython normal?
El Global Interpreter Lock (GIL) serializa la ejecución de bytecode para mantener seguro el conteo de referencias sin necesidad de un bloqueo alrededor de cada objeto individual; Python 3.13+ ofrece una compilación free-threaded experimental que elimina esta restricción a costa de una sobrecarga adicional de bloqueo por objeto.
¿Cómo decide Python cuándo liberar la memoria de un objeto?
Principalmente a través del conteo de referencias: en el momento en que el recuento de referencias de un objeto llega a cero, se libera inmediatamente, con un recolector de basura cíclico periódico que se encarga del caso más raro de objetos que se referencian mutuamente en un ciclo.
¿Por qué entender nombres frente a objetos importa más que memorizar sintaxis?
Porque las reglas de alcance, los errores de mutación compartida, el comportamiento de almacenamiento en caché de importaciones y la trampa de los argumentos predeterminados mutables son síntomas que parecen separados del mismo mecanismo subyacente, por lo que entender el mecanismo una vez explica todos ellos en lugar de memorizar cuatro reglas no relacionadas.
Relacionados
- Variables, Tipos y Tipado Dinámico - semántica de referencia e identidad de objeto en profundidad
- Flujo de Control - cómo se ejecutan las construcciones de ramificación y bucle dentro de este mismo modelo
- Funciones, Argumentos y Alcance - alcance LEGB y enlace de argumentos en la práctica
- Módulos, Importaciones y Paquetes - el caché de importación y los espacios de nombres de paquetes de cerca
- Fundamentos de Python - un recorrido práctico por la sintaxis en la que se basa este modelo
Versiones de Stack: Esta página fue escrita para Python 3.14 (estable) y Python 3.13 (mantenimiento), incluyendo las notas sobre la compilación free-threaded introducidas en 3.13 y estabilizadas aún más en 3.14.