Clase 6b — Scope: el ámbito de las variables
En la Clase 6a vimos en Python Tutor
que las variables pi
y radio aparecen
dentro del frame de
area_circulo
y desaparecen cuando la función
termina.
Si intentas acceder a ellas
desde fuera obtienes un
NameError.
Esto no es un capricho de Python — es una consecuencia de un sistema de reglas muy preciso que determina desde dónde se puede acceder a cada variable y cuándo existe. Ese sistema se llama scope o ámbito.
— El NameError inesperado —
# Variable definida dentro de una función
def area_circulo(radio):
pi = 3.1416
return pi * radio ** 2
area_circulo(5)
# Intentar acceder a pi desde fuera
try:
print(pi)
except NameError as e:
print(f"NameError: {e}")
# name 'pi' is not defined
# Lo mismo con el parámetro
try:
print(radio)
except NameError as e:
print(f"NameError: {e}")
# name 'radio' is not defined
— El scope tiene dos dimensiones —
Dónde — espacio
Desde qué partes del código se puede acceder a una variable. Una variable definida dentro de una función no es visible desde fuera — ni desde otras funciones.
Cuándo — tiempo
En qué momento de la ejecución existe una variable. Una variable local no existe antes de que se llame la función ni después de que termine.
# Dimensión DÓNDE — visibilidad
x = 10 # visible en todo el fichero
def f():
y = 20 # solo visible dentro de f()
print(x) # ✓ puede ver x — está fuera
print(y) # ✓ puede ver y — está dentro
f()
print(x) # ✓ visible desde fuera
try:
print(y) # ❌ y no existe aquí
except NameError as e:
print(f"NameError: {e}")
print("---")
# Dimensión CUÁNDO — existencia temporal
def g():
z = 30 # z nace aquí
print(f"z existe: {z}")
# z muere al terminar g()
# Antes de llamar a g(): z no existe
print("Antes de llamar")
g()
print("Después de llamar")
# Después de llamar a g(): z ya no existe
try:
print(z)
except NameError as e:
print(f"NameError: {e}")
— No todas las variables existen al mismo tiempo —
Este es el punto más contraintuitivo del scope para los principiantes: una variable puede existir en un momento de la ejecución y no existir en otro, aunque el código que la define ya haya sido procesado por Python.
Cuando Python procesa
def g():
crea el objeto función —
pero z no existe aún.
z solo nace cuando
se llama a g()
y muere cuando g()
termina.
Si llamas a g()
dos veces, z
nace y muere dos veces —
cada vez completamente independiente.
# z nace y muere en cada llamada — independiente
def contador_local():
z = 0
z += 1
print(f"z = {z}")
contador_local() # z = 1
contador_local() # z = 1 — empieza desde cero otra vez
contador_local() # z = 1 — siempre desde cero
# No "recuerda" el valor de la llamada anterior
# Cada llamada crea su propio z desde cero
# En la Clase 6c veremos closures — que sí recuerdan
NameError
en código de Python o de IA,
la causa casi siempre es una
de estas dos:
la variable está definida en un scope
distinto al que intenta acceder a ella,
o la variable está definida en la función
correcta pero se intenta usar
fuera de ella.
El scope explica ambos casos.
resultado = 0
def calcular():
valor = 42
return valor * 2
print("A")
print(calcular())
print("B")
print(valor)
print("C")
1 tres veces
en lugar de 1,
2, 3?
¿Cómo lo modificarías para
que acumule entre llamadas?
def incrementar():
n = 0
n += 1
print(n)
incrementar()
incrementar()
incrementar()
Imprime "A",
luego 84
— resultado de
calcular() —,
luego "B",
luego falla con
NameError:
name 'valor' is not defined.
El "C"
nunca se imprime.
valor es local
a calcular
— no existe fuera de ella.
Imprime 1
tres veces porque
n es local
a incrementar
— nace en 0
cada vez que se llama
y muere al terminar.
No hay memoria entre llamadas.
Para acumular hay dos opciones:
usar una variable global
(no recomendado)
o usar un closure —
que veremos en la Clase 6c:
# Opción simple — variable fuera de la función
n = 0
def incrementar():
global n
n += 1
print(n)
incrementar() # 1
incrementar() # 2
incrementar() # 3
Veremos por qué
global
no es la solución correcta
y qué alternativas existen
en la Parte II.
Variable fuera visible dentro → generalmente correcta — con matices que veremos en la Parte II.
Variable dentro visible fuera → falsa — es local, no existe fuera.
Variable local existe aunque la función nunca se llame → falsa — solo existe durante la ejecución de la función.
Dos llamadas comparten variables locales → falsa — cada llamada crea su propio espacio de variables completamente independiente.
El scope — o ámbito — es el conjunto de reglas que determina desde qué partes del programa se puede acceder a una variable y durante cuánto tiempo existe. No es una restricción arbitraria: es lo que permite que los programas grandes sean manejables — que una variable en una función no interfiera con otra variable del mismo nombre en otra función.
Sin scope, cualquier variable definida en cualquier parte del programa sería visible desde cualquier otro lugar. En un programa de 10 líneas eso no es un problema. En un programa de 10.000 líneas — o en el código que genera la IA — sería un caos inmanejable.
— Cinco razones concretas por las que importa —
1. Evitar NameError
Saber en qué scope vive cada variable permite entender por qué Python no la encuentra y dónde hay que definirla.
2. Funciones sin efectos laterales
Las variables locales no afectan
el resto del programa.
Puedes usar el nombre
x en diez funciones
distintas sin conflicto.
3. Funciones anidadas y closures
Las funciones anidadas pueden acceder a variables de la función exterior. Entender scope es el prerequisito para entender closures — Clase 6c.
4. Legibilidad y mantenimiento
El scope local hace que cada función sea independiente — puedes leer y modificar una función sin revisar el resto del programa.
5. Depuración más rápida
Cuando un programa falla, entender el scope permite acotar dónde puede estar el problema — si una variable local tiene un valor incorrecto, el error está dentro de esa función, no en el resto del programa.
— Scope y código de IA —
El scope es la fuente de algunos de los errores más frecuentes en código generado por IA. La IA a veces genera código que usa variables definidas en un scope distinto al que intenta acceder a ellas, o que modifica variables globales sin avisar. Reconocer estos patrones permite corregirlos antes de ejecutar el código.
# Error frecuente de IA — usar variable antes de definirla
def procesar(datos):
print(f"procesando {len(datos)} elementos")
resultado = [x * factor for x in datos]
# factor no está definido en ningún scope visible
return resultado
try:
procesar([1, 2, 3])
except NameError as e:
print(f"NameError: {e}")
print("---")
# Error frecuente de IA — confundir scope local con global
total = 0
def acumular(valor):
total = total + valor # ← error: total es local aquí
return total
try:
acumular(10)
except UnboundLocalError as e:
print(f"UnboundLocalError: {e}")
print("---")
# El mismo nombre en distintos scopes — sin conflicto
x = "global"
def f1():
x = "local en f1"
print(f"f1: {x}")
def f2():
x = "local en f2"
print(f"f2: {x}")
f1()
f2()
print(f"global: {x}") # sigue siendo "global"
NameError
específica del scope.
Ocurre cuando Python detecta
que una variable se asigna
dentro de una función
— y por tanto la trata como local —
pero se intenta usar antes
de que se le asigne un valor.
Es uno de los errores más confusos
para los principiantes porque
la variable existe globalmente
pero Python insiste en buscarla
como local.
Lo veremos en detalle en la Parte II.
i
como contador.
NameError
y el segundo un
UnboundLocalError?
¿En qué se diferencian?
f1 y f2
usan el mismo nombre x
que también existe globalmente.
¿Por qué no hay conflicto?
¿Qué pasaría si no existiera
el scope local?
Sin scope, todas las variables
compartirían el mismo espacio.
Si tres funciones usan
i como contador,
modificar i
en una afectaría a las otras.
Llamar a f1()
dentro de un bucle
podría reiniciar el contador
del bucle exterior.
Encontrar el error sería
casi imposible en un programa
grande porque cualquier
función podría ser la culpable.
El scope local garantiza
que el i de
cada función es suyo propio
— invisible desde fuera.
NameError:
factor no existe
en ningún scope —
ni local, ni global,
ni built-in.
Python buscó en todos
los niveles y no la encontró.
UnboundLocalError:
total sí existe
globalmente, pero Python
detecta que hay una asignación
total = total + valor
dentro de la función
— lo que la convierte
en variable local.
Python intenta acceder
a la versión local de
total
antes de que se haya asignado
— de ahí el error.
La variable existe globalmente
pero Python ni siquiera
la mira — busca la local.
No hay conflicto porque
cada función tiene
su propio scope local.
El x de
f1 y el
x de
f2 son variables
distintas que solo existen
dentro de cada función.
El x global
permanece intacto.
Sin scope local,
f1() y
f2()
modificarían el x
global — y el resultado
final dependería del orden
de las llamadas,
no de la lógica de cada función.
sorted()
sin preocuparte de que
sus variables internas
interfieran con las tuyas.
El scope local hace posible
esa abstracción.
El scope global es el nivel superior del fichero — fuera de cualquier función o clase. Todo lo que se define ahí es accesible desde cualquier parte del fichero, incluyendo el interior de las funciones.
Pero accesible no significa modificable.
Desde dentro de una función
puedes leer
una variable global sin problemas.
Si intentas asignarle
un nuevo valor,
Python no modifica la global —
crea una variable local nueva
con el mismo nombre.
Esta distinción es la fuente
de confusión más frecuente
con el scope.
Usar Python Tutor para ver cómo Python utiliza el scope
— globals(): inspeccionar el scope global —
# Todo lo definido en el nivel superior es global
x = "x global"
PI = 3.1416
nombres = ["Ana", "Bob"]
# globals() devuelve un dict con todo lo del scope global
print("x" in globals()) # True
print("PI" in globals()) # True
# Las funciones también son objetos globales
def mi_funcion():
pass
print("mi_funcion" in globals()) # True
# globals() desde dentro de una función
# — devuelve el mismo scope global
def ver_globals():
print("x" in globals()) # True — visible desde dentro
ver_globals()
— Leer una variable global desde dentro —
Una función puede leer variables globales sin ninguna declaración especial — Python las busca automáticamente en el scope global cuando no las encuentra en el local. Este es el comportamiento por defecto.
x = "x global"
def fx_global():
# x no está definida localmente
# Python la busca en el scope global
tipo_de_variable = x
print(tipo_de_variable)
fx_global() # "x global" — lee la variable global
print(x) # "x global" — sigue intacta
print("---")
# Ejemplo práctico — constante global
IVA = 0.21
def precio_con_iva(precio):
return precio * (1 + IVA) # lee IVA del scope global
def descuento_con_iva(precio, pct):
base = precio * (1 - pct / 100)
return base * (1 + IVA) # lee IVA del scope global
print(precio_con_iva(100))
print(descuento_con_iva(100, 10))
— Asignar dentro de una función crea una variable local —
Esta es la regla más importante
y la más contraintuitiva:
en cuanto Python ve una
asignación
(=) a un nombre
dentro de una función,
trata ese nombre como
variable local
en todo el cuerpo
de esa función —
incluso en las líneas anteriores
a la asignación.
No modifica la variable global. Crea una variable local nueva con el mismo nombre que solo existe dentro de la función. La global queda intacta.
x = "global"
# Caso 1: solo lectura — usa la global
def solo_leer():
print(x) # lee la global
solo_leer() # "global"
print(x) # "global" — intacta
print("---")
# Caso 2: asignación — crea una local nueva
def asignar():
x = "local" # ← crea una variable LOCAL llamada x
print(x) # imprime la local
asignar() # "local"
print(x) # "global" — la global no cambió
print("---")
# Caso 3: el error más confuso — UnboundLocalError
# La asignación hace que x sea LOCAL en todo el cuerpo
# incluso en las líneas ANTERIORES a la asignación
def error_confuso():
print(x) # ← intenta leer x LOCAL — aún no existe
x = "local" # ← esta línea hace que x sea local
try:
error_confuso()
except UnboundLocalError as e:
print(f"UnboundLocalError: {e}")
x = "local"
en cualquier parte del cuerpo,
decide que x es local
en toda la función.
Cuando luego intenta ejecutar
print(x)
en la línea anterior,
busca la versión local de x
— que aún no tiene valor —
y lanza UnboundLocalError.
La variable global existe,
pero Python ni la mira.
— locals(): inspeccionar el scope local —
def test(z, d="Default"):
# locals() devuelve un dict con las variables locales
print(locals())
print(z)
print(d)
test("local z")
# {'z': 'local z', 'd': 'Default'}
# los parámetros son variables locales
print("---")
def ver_locales(a, b):
c = a + b
print(locals()) # {'a': ..., 'b': ..., 'c': ...}
return c
ver_locales(3, 4)
# Bloque A
x = "global x"
def test_a():
y = "local y"
print(x)
test_a()
print(x)
try:
print(y)
except NameError as e:
print(f"Error: {e}")
# Bloque B
x = "global x"
def test_b():
x = "local x"
print(x)
test_b()
print(x)
UnboundLocalError
aunque total
existe globalmente?
¿Cómo lo corregirías
sin usar global?
total = 100
def añadir(valor):
total = total + valor
return total
print(añadir(50))
locals()
en esta función?
Predice antes de ejecutar:
def analizar(datos, umbral=0.5):
filtrados = [x for x in datos if x > umbral]
n = len(filtrados)
print(locals())
return filtrados
analizar([0.3, 0.7, 0.1, 0.9])
IVA = 0.21
es una práctica aceptable,
mientras que modificar
una variable global
desde dentro de una función
no lo es?
¿Cuál es la diferencia?
Bloque A:
test_a()
imprime "global x"
— lee la global porque
x no está
definida localmente.
print(x)
fuera imprime
"global x".
print(y)
lanza NameError
— y es local
a test_a
y no existe fuera.
Bloque B:
test_b()
imprime "local x"
— la asignación crea
una variable local.
print(x)
fuera imprime
"global x"
— la global no cambió.
Produce UnboundLocalError
porque total = total + valor
contiene una asignación
— Python trata total
como local en todo el cuerpo.
Al intentar evaluar
total + valor
busca la versión local
de total
que aún no tiene valor.
La corrección sin
global:
pasar total
como parámetro:
def añadir(total, valor):
return total + valor
total = 100
total = añadir(total, 50)
print(total) # 150
Esta versión es más clara y más segura — la función recibe el valor que necesita como argumento en lugar de depender de una variable global.
locals()
devuelve:
{'datos': [0.3, 0.7,
0.1, 0.9], 'umbral': 0.5,
'filtrados': [0.7, 0.9],
'n': 2}.
Contiene todos los parámetros
y todas las variables
definidas hasta ese punto
en la función —
incluyendo los valores
por defecto.
Una constante global
como IVA = 0.21
se lee
desde dentro de las funciones
pero nunca se modifica.
Es una fuente de información
centralizada —
si cambia el IVA,
se modifica en un solo lugar
y todas las funciones
lo ven automáticamente.
Modificar una variable global desde dentro de una función es peligroso porque crea un efecto lateral — la función cambia algo fuera de su propio scope sin que sea obvio al leerla. Si varias funciones modifican la misma global, el comportamiento del programa depende del orden de las llamadas — difícil de predecir y de depurar.
return.
El scope local es el espacio de nombres que Python crea cada vez que se llama a una función. Contiene todos los parámetros y todas las variables definidas dentro del cuerpo de esa función. Existe mientras la función se ejecuta y desaparece cuando termina.
Cada llamada a una función crea su propio scope local — completamente independiente de cualquier llamada anterior y de cualquier otra función. Dos llamadas a la misma función tienen sus propios scopes locales distintos.
— Variables locales: nacen y mueren con la función —
# Las variables locales no existen fuera
def fx_local():
x_l = "x_local"
print(x_l) # ✓ existe aquí
fx_local()
# Intentar acceder a x_l desde fuera
try:
y = x_l # ❌ x_l no existe aquí
except NameError as e:
print(f"NameError: {e}")
print("---")
# Los parámetros también son variables locales
def test(z, d="Default"):
print(locals()) # {'z': 'local z', 'd': 'Default'}
print(z)
print(d)
test("local z")
# z y d no existen fuera
try:
print(z)
except NameError as e:
print(f"NameError: {e}")
— Cada llamada crea su propio scope independiente —
Cuando llamas a una función
dos veces, Python crea dos scopes
locales distintos — uno para
cada llamada.
Las variables de una llamada
no interfieren con las de la otra.
Cada scope nace con la llamada
y muere con el return.
def procesar(nombre, valor):
resultado = valor * 2
print(f"{nombre}: valor={valor}, resultado={resultado}")
return resultado
# Cada llamada tiene su propio scope
# — sus propios nombre, valor, resultado
procesar("primera", 10)
procesar("segunda", 25)
procesar("tercera", 7)
print("---")
# Demostración: cada llamada empieza desde cero
def acumular_local():
total = 0 # nace en 0 cada vez
total += 1
total += 1
total += 1
print(f"total: {total}") # siempre 3
acumular_local() # total: 3
acumular_local() # total: 3 — empieza de cero
acumular_local() # total: 3 — empieza de cero
— Los tres casos: leer global, definir local, el error —
Estos son los tres casos
de tus slides — el más importante
es el Caso 3, que produce
el UnboundLocalError
más confuso de Python.
La clave es que Python analiza
el cuerpo completo antes
de ejecutarlo — si ve una asignación,
trata la variable como local
en todo el cuerpo,
incluso antes de la asignación.
x = "global x"
# Caso 1: x no está definida localmente
# Python la busca en el scope global
def test_1():
y = "local y"
print(x) # lee la global
test_1() # "global x"
print(x) # "global x" — intacta
try:
print(y) # NameError — y es local a test_1
except NameError as e:
print(f"NameError: {e}")
print("---")
# Caso 2: x definida localmente
# La local oculta la global dentro de la función
def test_2():
x = "local x" # crea una local nueva
print(x) # "local x"
test_2()
print(x) # "global x" — la global no cambió
print("---")
# Caso 3: el error más confuso
# La asignación en línea 3 hace que x sea local
# en TODO el cuerpo — incluyendo la línea 2
def test_3():
print(x) # ← intenta leer x LOCAL — no existe aún
x = "local" # ← esta línea hace que x sea local en todo
try:
test_3()
except UnboundLocalError as e:
print(f"UnboundLocalError: {e}")
Por qué ocurre el Caso 3 — paso a paso
test_3
antes de ejecutarlo.x = "local"
— una asignación a x.x es local
en toda la función
— incluso antes de la asignación.print(x)
→ busca x local
→ no existe aún
→ UnboundLocalError.
— Verlo en Python Tutor —
Pega los tres casos en
pythontutor.com
y obsérvalos paso a paso.
En el Caso 1: verás que
el frame de test_1
no contiene x
— Python la busca y la encuentra
en el frame global.
En el Caso 2: verás dos
x distintas —
una en el frame local
y otra en el global.
En el Caso 3: verás cómo
el frame se crea con x
marcada como local
pero sin valor asignado aún.
precio = 100
def aplicar_descuento(pct):
descuento = precio * pct / 100
precio_final = precio - descuento
print(f"descuento: {descuento:.2f}€")
print(f"precio final: {precio_final:.2f}€")
aplicar_descuento(10)
print(f"precio original: {precio}")
UnboundLocalError?
¿Cómo lo corregirías?
precio = 100
def aplicar_iva():
print(f"precio sin IVA: {precio}")
precio = precio * 1.21
print(f"precio con IVA: {precio:.2f}")
aplicar_iva()
x
cuando se ejecuta
print(x)?
En el Caso 2: ¿cuántas
variables x
existen simultáneamente?
En el Caso 3: ¿en qué paso
aparece el error?
umbral = 50
def clasificar(valor):
if valor > umbral:
categoria = "alto"
print(f"{valor} → {categoria}")
return categoria
print(clasificar(75))
print(clasificar(30))
No produce error.
precio
se lee del scope global
— no hay asignación
a precio
dentro de la función,
solo lectura.
descuento
y precio_final
son locales.
Imprime:
descuento: 10.00€,
precio final: 90.00€,
precio original: 100
— la global no cambió.
UnboundLocalError
porque precio = precio * 1.21
contiene una asignación
— Python trata precio
como local en todo el cuerpo.
El print
de la primera línea
intenta leer la local
que aún no existe.
La corrección correcta:
pasar precio
como parámetro:
def aplicar_iva(precio):
print(f"precio sin IVA: {precio}")
precio_con_iva = precio * 1.21
print(f"precio con IVA: {precio_con_iva:.2f}")
return precio_con_iva
aplicar_iva(100)
Caso 1: x
está en el frame global
— Python la busca ahí
porque no la encuentra
en el frame local de
test_1.
Caso 2: existen dos
x simultáneamente
— una en el frame local
de test_2
con valor "local x"
y otra en el frame global
con valor "global x".
Caso 3: el error aparece
en la primera instrucción
ejecutable del cuerpo
— print(x)
— porque x
fue marcada como local
antes de ejecutar.
El error está en que
categoria
solo se define si
valor > umbral.
Cuando valor = 30,
no se entra en el
if
y categoria
no se define nunca.
El print
intenta acceder a una
variable local que
no existe —
UnboundLocalError.
La corrección:
def clasificar(valor):
if valor > umbral:
categoria = "alto"
else:
categoria = "bajo" # ← siempre definida
print(f"{valor} → {categoria}")
return categoria
if
sin el else
correspondiente.
Si la condición no se cumple,
la variable no existe
y cualquier uso posterior
produce un error.
Siempre verifica que
las variables locales
se definen en todas
las ramas posibles.
global
y
nonlocal:
modificar variables de otros scopes
teoría
Por defecto una función no puede
modificar variables de scopes
superiores — solo leerlas.
Python proporciona dos palabras clave
para cambiar ese comportamiento:
global para modificar
variables del scope global,
y nonlocal para modificar
variables del scope enclosing
— que veremos en detalle
en la Parte III.
Ambas existen pero ambas
se desaconsejan en la mayoría
de situaciones.
Conocerlas es importante
para leer código ajeno y de IA —
pero la alternativa correcta
casi siempre es pasar
el valor como parámetro
y devolver el resultado con
return.
— global: modificar una variable del scope global —
global x dentro de una función
le dice a Python:
"cuando veas x en esta función,
no crees una variable local —
usa la x del scope global
y modifícala directamente".
# Sin global — crea una local, no modifica la global
x = "global"
def sin_global():
x = "local" # crea una local nueva
print(f"dentro: {x}")
sin_global()
print(f"fuera: {x}") # "global" — no cambió
print("---")
# Con global — modifica la variable global
x = "global"
def con_global():
global x
x = "modificada" # modifica la global
print(f"dentro: {x}")
con_global()
print(f"fuera: {x}") # "modificada" — cambió
print("---")
# global también funciona si la variable no existe aún
def crear_global():
global nueva_variable
nueva_variable = "creada desde dentro"
print(f"dentro: {nueva_variable}")
crear_global()
print(f"fuera: {nueva_variable}") # existe ahora
— Por qué global no se recomienda —
Modificar variables globales
desde funciones crea
efectos laterales:
la función cambia algo fuera
de su propio scope.
Esto hace el código difícil
de entender y de depurar —
si x tiene un valor
inesperado, tienes que buscar
en todas las funciones que usan
global x
para encontrar cuál lo cambió.
La alternativa correcta
casi siempre existe y es más clara:
pasar el valor como parámetro
y devolver el resultado
con return.
# ❌ Con global — efecto lateral oculto
contador = 0
def incrementar_mal():
global contador
contador += 1
incrementar_mal()
incrementar_mal()
incrementar_mal()
print(f"contador: {contador}") # 3
# ✓ Sin global — más claro y predecible
def incrementar_bien(n):
return n + 1
contador = 0
contador = incrementar_bien(contador)
contador = incrementar_bien(contador)
contador = incrementar_bien(contador)
print(f"contador: {contador}") # 3
print("---")
# El problema con global en programas grandes
total = 0
def sumar(x):
global total
total += x # ¿quién llamó a sumar?
# ¿cuántas veces? ¿en qué orden?
def restar(x):
global total
total -= x # dos funciones modifican la misma global
sumar(10)
restar(3)
sumar(5)
print(f"total: {total}") # 12 — pero ¿por qué?
# Difícil de seguir sin leer todas las llamadas
— nonlocal: anticipo del enclosing scope —
nonlocal es el equivalente
de global pero para
el scope enclosing — la función
exterior que contiene la función actual.
Se usa en funciones anidadas
para modificar variables
de la función exterior.
Lo veremos en detalle en la Parte III
con el LEGB completo.
Por ahora es suficiente saber
que existe y para qué sirve —
verás la diferencia con
global claramente
en el ejemplo.
# global — modifica el scope GLOBAL
x = "global"
def outer_global():
x = "outer"
def inner():
global x # modifica la x GLOBAL
x = "modificada"
print(f"inner: {x}")
inner()
print(f"outer: {x}") # "outer" — no cambió
outer_global()
print(f"global: {x}") # "modificada" — sí cambió
print("---")
# nonlocal — modifica el scope ENCLOSING (outer)
x = "global"
def outer_nonlocal():
x = "outer"
def inner():
nonlocal x # modifica la x de outer
x = "modificada"
print(f"inner: {x}")
inner()
print(f"outer: {x}") # "modificada" — sí cambió
outer_nonlocal()
print(f"global: {x}") # "global" — no cambió
Pega los dos últimos bloques
en
pythontutor.com
y obsérvalos paso a paso.
Con global:
observa cómo inner
salta el frame de outer
y modifica directamente
el frame global.
Con nonlocal:
observa cómo inner
modifica la x
del frame de outer
— no la global.
La diferencia entre los dos frames
que cambian es la clave.
— Resumen: global vs nonlocal —
| Palabra clave | Modifica | Cuándo usarla | Recomendación |
|---|---|---|---|
global
|
Variable del scope global | Raramente — casi siempre hay una alternativa mejor | No recomendado |
nonlocal
|
Variable del scope enclosing | En closures — Clase 6c | Con moderación |
x = "global"
def fx():
global x
x = "modificada"
print(f"dentro: {x}")
print(f"antes: {x}")
fx()
print(f"después: {x}")
global
para funcionar correctamente?
IVA = 0.21
def calcular_precio(base):
return base * (1 + IVA)
print(calcular_precio(100))
global
y usando return
en su lugar:
ventas_totales = 0
def registrar_venta(cantidad):
global ventas_totales
ventas_totales += cantidad
registrar_venta(150)
registrar_venta(80)
registrar_venta(200)
print(f"total: {ventas_totales}")
global
vs nonlocal:
¿qué variable cambia
en cada caso?
¿Qué imprime cada versión
al final?
Ejecuta ambas en Python Tutor
y observa qué frame
se modifica en cada paso.
Imprime:
antes: global,
luego ejecuta fx()
que imprime
dentro: modificada,
luego después: modificada.
La variable global
x cambia
permanentemente después
de llamar a fx().
No necesita global
porque solo lee
IVA — no lo modifica.
Leer una variable global
desde dentro de una función
funciona automáticamente
sin ninguna declaración especial.
global solo es
necesario cuando quieres
asignar
un nuevo valor a la variable global.
def registrar_venta(ventas_totales, cantidad):
return ventas_totales + cantidad
ventas_totales = 0
ventas_totales = registrar_venta(ventas_totales, 150)
ventas_totales = registrar_venta(ventas_totales, 80)
ventas_totales = registrar_venta(ventas_totales, 200)
print(f"total: {ventas_totales}") # 430
La función ahora es pura — recibe todo lo que necesita como parámetro y devuelve el resultado sin efectos laterales. Es más fácil de probar, de depurar y de reutilizar.
global
en código de IA,
pregúntate si se puede
eliminar pasando el valor
como parámetro y devolviendo
el resultado.
En el 90% de los casos
la respuesta es sí.
Con global:
cambia la x
del frame global.
La x de
outer_global
no cambia.
Imprime:
inner: modificada,
outer: outer,
global: modificada.
Con nonlocal:
cambia la x
del frame de
outer_nonlocal.
La x global
no cambia.
Imprime:
inner: modificada,
outer: modificada,
global: global.
global salta
todos los frames intermedios
y va directamente al global.
nonlocal sube
un nivel — al frame
de la función inmediatamente
exterior.
En Python Tutor se ve
claramente cuál de los
tres frames cambia
en cada caso.
Cuando Python encuentra un nombre
— una variable, una función,
cualquier identificador —
lo busca en cuatro niveles
en un orden muy preciso.
Se detiene en el primero
donde lo encuentra.
Si no lo encuentra en ninguno,
lanza NameError.
El orden se conoce como la regla LEGB: Local → Enclosing → Global → Built-in. Es la regla más importante del scope en Python — explica todos los comportamientos que hemos visto en la Parte II y los que veremos en la Parte III.
— Los cuatro niveles —
L — Local
Variables definidas dentro de la función actual — incluyendo sus parámetros. Es el primer lugar donde Python busca. Si lo encuentra aquí, no busca más.
E — Enclosing
Variables de la función exterior que contiene la función actual. Solo existe cuando hay funciones anidadas. Python busca aquí si no lo encontró localmente.
G — Global
Variables definidas en el nivel superior del fichero — fuera de cualquier función. Python busca aquí si no lo encontró ni local ni en el enclosing.
B — Built-in
Nombres predefinidos de Python:
len, print,
sum, range,
type...
Siempre disponibles.
Python busca aquí como
último recurso.
— Los cuatro niveles en acción —
# Built-in — siempre disponible
print(len([1, 2, 3])) # B: len viene del scope built-in
# Global
x = "global" # G: definida en el nivel superior
def outer():
x = "enclosing" # E: visible desde inner()
def inner():
x = "local" # L: definida en inner()
print(f"inner ve: {x}") # L gana
inner()
print(f"outer ve: {x}") # E — su propia local
outer()
print(f"global ve: {x}") # G — la global
print("---")
# El orden de búsqueda — Python se detiene al encontrarlo
nombre = "global"
def demostrar_legb():
# Python busca 'nombre' en este orden:
# 1. Local — no está definido aquí
# 2. Enclosing — no hay función exterior
# 3. Global — ✓ encontrado aquí
print(nombre) # "global"
# Python busca 'len' en este orden:
# 1. Local — no está
# 2. Enclosing — no hay
# 3. Global — no está
# 4. Built-in — ✓ encontrado aquí
print(len([1, 2])) # 2
demostrar_legb()
— El nivel Built-in: el nivel invisible —
El nivel Built-in contiene todos los nombres predefinidos de Python — funciones, tipos y constantes que están siempre disponibles sin importar nada. Es el nivel más bajo de la búsqueda — Python solo llega aquí si no encontró el nombre en ningún otro nivel.
El peligro: si defines una variable
con el mismo nombre que una función
built-in, la ocultas
en ese scope y pierdes el acceso
a la función original.
Es un error frecuente — especialmente
con nombres como list,
sum, input
o type.
# El módulo builtins contiene todos los nombres del nivel B
import builtins
print(dir(builtins)[:10]) # los primeros 10
print("---")
# Ocultar un built-in con una variable del mismo nombre
# ❌ Nunca hagas esto
print(len([1, 2, 3])) # 3 — funciona
len = 42 # ← oculta la función len con un entero
try:
print(len([1, 2, 3])) # TypeError — len ahora es 42
except TypeError as e:
print(f"TypeError: {e}")
del len # restaurar — eliminar la variable local
print(len([1, 2, 3])) # 3 — vuelve a funcionar
print("---")
# Lo mismo dentro de una función — más peligroso
def procesar(datos):
print(sum(datos)) # ✓ usa built-in sum
sum = 0 # ← oculta sum en este scope
for x in datos:
sum += x
print(sum) # imprime el total manual
# a partir de aquí sum es el entero, no la función
procesar([1, 2, 3, 4])
list, dict,
set, tuple,
sum, min,
max, len,
type, input,
print, range,
id, open.
La IA comete este error con frecuencia
— especialmente con list,
sum y input.
Si ves que una de estas funciones
deja de funcionar en un script,
busca si hay una variable con
el mismo nombre definida antes.
Pega el primer bloque completo
en
pythontutor.com
y avanza paso a paso.
Observa los frames activos
en cada momento:
cuando se ejecuta inner()
hay tres frames simultáneos —
inner,
outer
y el global.
Cada uno tiene su propia
x.
Python busca de abajo a arriba
en la pila de frames —
de local a global.
La x que ve
inner es la suya propia
— no necesita subir más.
x
en este código, indica en qué
nivel LEGB lo encuentra Python:
x = "G"
def outer():
x = "E"
def inner():
print(x) # ¿L, E, G o B?
inner()
print(x) # ¿L, E, G o B?
outer()
print(x) # ¿L, E, G o B?
resultado = "global"
def f():
print(resultado)
def g():
resultado = "local de g"
f()
g()
print(resultado)
Atención: ¿qué scope ve
f() cuando
es llamada desde g()?
def analizar(datos):
list = [x * 2 for x in datos]
resultado = sum(list)
return sorted(list), resultado
print(analizar([3, 1, 4, 1, 5]))
print(x)
dentro de inner:
Python busca en L — no está,
busca en E — ✓ encontrada
en outer
con valor "E".
Imprime "E".
print(x)
dentro de outer:
Python busca en L —
✓ encontrada localmente
con valor "E".
Imprime "E".
print(x)
fuera: Python busca en G —
✓ encontrada con valor
"G".
Imprime "G".
Imprime "global"
dos veces.
Este es el comportamiento
más sorprendente del scope:
f() ve el scope
donde fue definida
— no donde fue llamada.
f fue definida
en el nivel global,
así que busca
resultado
en el scope global —
donde vale "global".
No ve el resultado
local de g()
aunque fue llamada desde ahí.
El error es usar
list
como nombre de variable —
oculta la función built-in
list
en el scope local.
En este caso concreto
el código funciona
porque list
no se usa como función
después de la asignación,
pero es una mala práctica
que puede causar errores
difíciles de detectar.
La corrección:
def analizar(datos):
dobles = [x * 2 for x in datos] # nombre descriptivo
resultado = sum(dobles)
return sorted(dobles), resultado
print(analizar([3, 1, 4, 1, 5]))
Si Python buscara primero
en Built-in, nunca podrías
definir una variable llamada
x o n
si existieran en Built-in,
porque la built-in
siempre ganaría.
Peor aún: no podrías
sobreescribir built-ins
intencionalmente cuando
tiene sentido hacerlo.
Buscar primero en Local
y último en Built-in
garantiza que el código
más cercano al punto
de uso tiene prioridad
— que es lo intuitivo.
nonlocal
teoría
El scope enclosing — la E de LEGB — aparece cuando una función está definida dentro de otra. La función interior puede acceder a las variables de la función exterior sin ninguna declaración especial. Python las busca automáticamente en el nivel enclosing si no las encuentra localmente.
Es el nivel más sutil de LEGB y el prerequisito para entender los closures de la Clase 6c. La pregunta clave es siempre: ¿desde dónde fue definida esta función? La respuesta determina qué variables tiene disponibles en su scope enclosing.
— La función interior ve las variables de la exterior —
# La función interior puede leer variables del exterior
def exterior():
mensaje = "Hola desde la función exterior"
def interior():
print(mensaje) # E: lee del scope enclosing
interior()
exterior() # "Hola desde la función exterior"
print("---")
# Los dos casos de las slides: con y sin x local en inner
def outer():
x = "outer x"
# Caso 1: inner tiene su propia x local
def inner_con_local():
x = "inner x" # L: oculta la del enclosing
print(f"inner: {x}")
inner_con_local()
print(f"outer: {x}") # "outer x" — no cambió
outer()
print("---")
def outer():
x = "outer x"
# Caso 2: inner no tiene x local — busca en enclosing
def inner_sin_local():
print(f"inner: {x}") # E: usa la del enclosing
inner_sin_local()
print(f"outer: {x}") # "outer x"
outer()
— nonlocal: modificar la variable del enclosing —
Igual que global
permite modificar variables globales,
nonlocal permite
modificar variables del scope
enclosing — la función exterior.
Sin nonlocal,
cualquier asignación dentro
de la función interior crea
una variable local nueva.
Con nonlocal,
la asignación modifica
la variable de la función exterior.
# Sin nonlocal — la asignación crea una local
def outer_sin():
x = "outer x"
def inner():
x = "inner x" # crea una local nueva
print(f"inner: {x}")
inner()
print(f"outer: {x}") # "outer x" — no cambió
outer_sin()
print("---")
# Con nonlocal — modifica la variable del enclosing
def outer_con():
x = "outer x"
def inner():
nonlocal x
x = "inner x" # modifica la del enclosing
print(f"inner: {x}")
inner()
print(f"outer: {x}") # "inner x" — sí cambió
outer_con()
print("---")
# Con global y la x global — para comparar
x = "x global"
def outer_global():
x = "outer x"
def inner():
nonlocal x
x = "inner x"
print(f"inner: {x}")
inner()
print(f"outer: {x}") # "inner x" — nonlocal modificó
outer_global()
print(f"global: {x}") # "x global" — nonlocal no toca la global
— El caso más confuso: asignar antes de leer en enclosing —
La misma regla del Caso 3 de la Parte II se aplica al scope enclosing: si Python ve una asignación a un nombre dentro de la función interior, lo trata como local en todo el cuerpo de esa función — incluso si hay una variable con ese nombre en el enclosing.
a = 0 # global
# Caso 1: lee del global — funciona
def caso_1():
print(a) # G: lee la global
caso_1() # 0
print("---")
# Caso 2: define local — oculta la global
def caso_2():
a = 3
print(a) # L: usa la local
caso_2() # 3
print(a) # 0 — global intacta
print("---")
# Caso 3: el error — asignación después de lectura
# Python ve 'a = 3' → trata 'a' como local en todo el cuerpo
# → el print(a) anterior intenta leer la local → no existe aún
def caso_3():
print(a) # intenta leer a LOCAL — no existe aún
a = 3 # hace que a sea LOCAL en todo el cuerpo
try:
caso_3()
except UnboundLocalError as e:
print(f"UnboundLocalError: {e}")
Pega los dos bloques principales
— los dos casos con y sin local
y los dos casos con y sin
nonlocal —
en
pythontutor.com
y obsérvalos paso a paso.
En los casos sin nonlocal:
verás que cuando inner
busca x sin encontrarla
localmente, Python sube al frame
de outer — el enclosing.
En los casos con nonlocal:
verás que la asignación modifica
directamente la x
del frame de outer.
Observa cuántos frames hay activos
simultáneamente y en qué orden
Python los recorre.
print
indica en qué nivel LEGB
encuentra Python la variable:
x = "global"
def outer():
x = "outer"
def middle():
x = "middle"
def inner():
print(x) # ?
inner()
print(x) # ?
middle()
print(x) # ?
outer()
print(x) # ?
nonlocal
no afecta a la variable global?
x = "global"
def outer():
x = "outer"
def inner():
nonlocal x
x = "inner"
print(f"inner: {x}")
inner()
print(f"outer: {x}")
outer()
print(f"global: {x}")
caso_1,
caso_2,
caso_3)
en Python Tutor.
En el Caso 3:
¿en qué paso exacto
aparece el error?
¿Qué muestra Python Tutor
sobre el frame de
caso_3
antes del error?
def hacer_multiplicador(factor):
def multiplicar(x):
return x * factor
return multiplicar
doble = hacer_multiplicador(2)
triple = hacer_multiplicador(3)
print(doble(5))
print(triple(5))
print(doble(10))
print(triple(10))
print(x)
en inner:
L — no está,
E — ✓ encontrada en
middle
con valor "middle".
Imprime "middle".
print(x)
en middle:
L — ✓ encontrada localmente
con valor "middle".
Imprime "middle".
print(x)
en outer:
L — ✓ encontrada localmente
con valor "outer".
Imprime "outer".
print(x)
fuera:
G — ✓ encontrada globalmente
con valor "global".
Imprime "global".
middle,
luego outer,
luego global, luego built-in.
Imprime:
inner: inner,
outer: inner,
global: global.
nonlocal
no afecta a la variable global
porque opera exactamente
un nivel hacia arriba —
al scope enclosing inmediato,
que es outer,
no el global.
nonlocal x
le dice a Python:
"modifica la x
de la función que me contiene
directamente" —
que es outer,
no el nivel global.
La global queda intacta.
El error aparece
en el primer paso
de la ejecución del cuerpo
de caso_3
— al intentar ejecutar
print(a).
Python Tutor muestra
el frame de caso_3
con a
marcada como variable local
pero sin valor asignado.
La variable existe
en el frame — Python sabe
que será local —
pero no tiene valor aún.
Es diferente a no existir:
existe como local
pero está
unbound —
sin valor.
Imprime: 10,
15,
20,
30.
No hay problema de scope —
este es un ejemplo perfecto
del uso correcto del scope
enclosing.
multiplicar
accede a factor
del scope enclosing
de hacer_multiplicador.
Lo interesante es que
doble y
triple
son dos funciones distintas
— cada una con su propio
factor
en el enclosing scope:
2 para
doble
y 3
para triple.
Esto es exactamente
un closure
— la función
multiplicar
recuerda el valor de
factor
aunque hacer_multiplicador
ya terminó de ejecutarse.
Lo veremos en profundidad
en la Clase 6c.
hacer_multiplicador
es una factory
de funciones — devuelve
funciones configuradas
con distintos valores
de factor.
El scope enclosing es
exactamente lo que hace
posible este patrón.
A lo largo de esta clase hemos visto varios comportamientos que parecen misteriosos si no se conoce el mecanismo detrás: las variables locales desaparecen al terminar la función, dos llamadas a la misma función tienen variables completamente independientes, y Python puede tener varias funciones "en marcha" al mismo tiempo cuando una llama a otra.
Todo esto tiene una explicación concreta en cómo Python gestiona la memoria durante la ejecución. No hace falta saber nada de informática para entenderlo — las analogías son suficientes para construir el modelo mental correcto.
— El frame: la hoja de trabajo de una función —
Imagina que cada vez que Python llama a una función, coge una hoja de papel en blanco y la pone sobre la mesa. En esa hoja anota todas las variables que la función necesita: los parámetros con sus valores, y las variables locales a medida que se van creando.
Mientras la función se ejecuta,
Python trabaja sobre esa hoja.
Cuando la función termina —
cuando llega al return
o al final del cuerpo —
Python coge la hoja
y la tira a la papelera.
Todo lo que estaba anotado en ella
desaparece.
Eso es lo que llamamos
un frame.
def area_circulo(radio):
# Al llamar: Python crea un frame nuevo
# En el frame anota: radio = 5
pi = 3.1416
# En el frame anota: pi = 3.1416
resultado = pi * radio ** 2
# En el frame anota: resultado = 78.54
return resultado
# return: devuelve el valor y TIRA el frame
# radio, pi y resultado desaparecen
area = area_circulo(5)
print(area) # 78.54 — el valor devuelto sí persiste
# Las variables del frame ya no existen
try:
print(pi)
except NameError as e:
print(f"NameError: {e}") # pi no existe
print("---")
# Cada llamada crea su propio frame — independiente
print(area_circulo(1)) # frame con radio=1
print(area_circulo(3)) # frame nuevo con radio=3
print(area_circulo(5)) # frame nuevo con radio=5
# Tres frames distintos, tres hojas de papel distintas
— El stack: la pila de frames activos —
Cuando una función llama a otra, Python no tira la primera hoja — la deja sobre la mesa y pone una hoja nueva encima. La mesa tiene entonces una pila de hojas: la de arriba es la función que se está ejecutando ahora, la de abajo es la que la llamó.
Cuando la función de arriba termina, Python tira esa hoja y vuelve a trabajar con la de abajo — que estaba esperando. A esta pila de hojas se le llama call stack o simplemente stack. Siempre hay al menos una hoja — la del programa principal.
def calcular_circulo(radio):
# Frame 2 — encima del frame global
area = area_circulo(radio) # llama a otra función
# Frame 3 — encima del frame 2
# Frame 3 termina, vuelve al frame 2
return f"radio={radio}, area={area:.2f}"
def area_circulo(radio):
# Frame 3 — mientras se ejecuta esto
# el stack tiene: global → calcular_circulo → area_circulo
pi = 3.1416
return pi * radio ** 2
# return: tira frame 3, vuelve a frame 2
# Frame global — siempre presente
resultado = calcular_circulo(5)
print(resultado)
print("---")
# El traceback que muestra Python cuando hay un error
# ES la pila de frames — de abajo a arriba
def nivel_1():
nivel_2()
def nivel_2():
nivel_3()
def nivel_3():
return 1 / 0 # error aquí
try:
nivel_1()
except ZeroDivisionError as e:
print(f"Error: {e}")
print("El traceback muestra el stack completo:")
print(" global → nivel_1 → nivel_2 → nivel_3")
File "...", line N, in funcion,
está mostrando la pila de frames
activos en el momento del error —
de más abajo a más arriba.
La última línea es donde ocurrió
el error. Las anteriores son
las funciones que lo llamaron.
Entender el stack permite leer
cualquier traceback sin confusión.
— El heap: el almacén de objetos —
Los frames contienen los nombres de las variables y referencias a sus valores — pero no los valores en sí si son objetos complejos. Las listas, los strings, los dicts, las funciones mismas — todos viven en una zona de memoria separada llamada heap.
La analogía: en el frame (la hoja de papel) anotas el nombre de la variable y la dirección de un almacén. El objeto real — la lista, el string — está en ese almacén. Cuando tiras el frame, tiras la hoja con la dirección, pero el almacén puede seguir existiendo si alguien más tiene su dirección anotada.
def crear_lista():
# En el frame: mi_lista → referencia al objeto lista en el heap
mi_lista = [1, 2, 3]
return mi_lista
# El frame se tira — pero la lista sigue en el heap
# porque alguien más tiene su referencia
lista_global = crear_lista()
# lista_global → misma lista en el heap
print(lista_global) # [1, 2, 3] — sigue existiendo
print("---")
# La función también vive en el heap
def mi_funcion():
return 42
# En el frame global: mi_funcion → referencia al objeto función en el heap
print(type(mi_funcion)) #
print(id(mi_funcion)) # dirección en el heap
print("---")
# El heap persiste mientras haya referencias
# Si nadie apunta al objeto, Python lo elimina (garbage collection)
def sin_referencia():
datos = [1, 2, 3] # lista en el heap
return None # no devuelve la lista # al terminar: el frame se tira
# la lista en el heap ya no tiene referencias
# Python la eliminará automáticamente
sin_referencia()
# La lista [1,2,3] ya no existe en ningún sitio
— Resumen: frame, stack y heap —
| Concepto | Analogía | Qué contiene | Cuándo existe |
|---|---|---|---|
| Frame | Hoja de papel | Variables locales y parámetros | Durante la ejecución de la función |
| Stack | Pila de hojas | Todos los frames activos en cada momento | Siempre — al menos el frame global |
| Heap | Almacén general | Objetos complejos: listas, strings, funciones | Mientras alguien tenga una referencia |
Pega el ejemplo de
calcular_circulo
en
pythontutor.com
y avanza paso a paso.
Observa tres cosas:
en la columna izquierda verás
el stack — los frames apilados,
con el más reciente arriba.
En la columna derecha verás
el heap — los objetos complejos
como flechas apuntando
a sus valores.
Cuando una función termina,
observa cómo su frame desaparece
del stack y el control vuelve
al frame anterior.
nivel_3()
en el ejemplo del traceback?
¿Cuál está arriba y cuál abajo?
1 tres veces
en lugar de 1,
2, 3:
def contar():
n = 0
n += 1
print(n)
contar()
contar()
contar()
crear_lista()
sigue existiendo después
de que la función termine,
pero la lista de
sin_referencia()
no?
¿Qué tienen en común
y en qué se diferencian?
calcular_circulo
en Python Tutor.
¿Cuántos frames hay en el stack
en el momento en que se ejecuta
pi = 3.1416
dentro de area_circulo?
Descríbelos de arriba a abajo.
Hay cuatro frames en el stack:
arriba del todo
nivel_3
— donde ocurre el error —,
debajo nivel_2
— que llamó a nivel_3 —,
debajo nivel_1
— que llamó a nivel_2 —,
y en la base el frame global
— que llamó a nivel_1.
El traceback los muestra
de abajo a arriba —
de global a nivel_3 —
para que el error aparezca
al final, que es donde
el programador mira primero.
Con la analogía de las hojas:
cada vez que se llama a
contar(),
Python coge una hoja nueva
en blanco y anota
n = 0.
Suma 1 — anota n = 1.
Imprime 1.
Tira la hoja.
La siguiente llamada
coge otra hoja nueva en blanco
— no hay rastro de la anterior.
n empieza en 0
otra vez.
Cada llamada es un estado
completamente nuevo —
no hay memoria entre llamadas.
En ambos casos la lista se crea en el heap durante la ejecución de la función. La diferencia está en lo que pasa al terminar:
crear_lista()
devuelve la lista con
return
— el frame global recibe
la referencia y la guarda
en lista_global.
La lista sigue teniendo
alguien que la apunta
— sigue viva.
sin_referencia()
devuelve None
— nadie guarda la referencia
a la lista.
Cuando el frame se tira,
la referencia desaparece.
La lista en el heap
ya no tiene nadie
que la apunte —
Python la elimina
automáticamente.
return
y guárdalo en una variable.
Sin return
— o sin guardar el resultado —
el objeto desaparece
con el frame.
Hay tres frames en el stack:
arriba area_circulo
— donde se está ejecutando
pi = 3.1416 —,
en el medio
calcular_circulo
— que llamó a
area_circulo
y está esperando
su resultado —,
y en la base el frame global
— que llamó a
calcular_circulo.
Python Tutor los muestra
apilados en la columna
izquierda — exactamente
la pila de hojas de papel.
Ahora que sabemos qué son el frame y el stack, todos los comportamientos del scope que hemos visto en las Partes II y III tienen una explicación mecánica concreta. No son reglas arbitrarias — son consecuencias directas de cómo Python gestiona la memoria.
La regla LEGB vista desde los frames: cuando Python busca un nombre, mira primero en el frame activo (Local), luego en los frames de las funciones exteriores que están debajo en el stack (Enclosing), luego en el frame global que siempre está en la base (Global), y finalmente en el espacio de nombres de Python (Built-in).
— Scope local = el frame activo —
Las variables locales viven en el frame de la función. Cuando el frame desaparece al terminar la función, las variables desaparecen con él. Por eso no existen fuera. Por eso dos llamadas son independientes — cada una tiene su propio frame, su propia hoja de papel.
def f(x):
y = x * 2
print(f"dentro del frame: x={x}, y={y}")
return y
# frame desaparece — x e y ya no existen
resultado = f(5)
print(f"fuera del frame: resultado={resultado}")
try:
print(x) # NameError — x vivía en el frame
except NameError as e:
print(f"NameError: {e}")
print("---")
# Dos llamadas — dos frames — dos x independientes
f(3) # frame con x=3
f(7) # frame nuevo con x=7 — no tiene nada que ver con el anterior
— Scope global = el frame que nunca desaparece —
El scope global vive en un frame especial — el frame del programa principal — que se crea al arrancar el script y solo desaparece cuando el programa termina. Es el frame de la base del stack que siempre está ahí.
Cuando Python busca un nombre y no lo encuentra en el frame de la función actual, salta directamente a este frame base y busca ahí. Por eso las funciones pueden leer variables globales sin declaración especial.
PI = 3.1416 # vive en el frame global — siempre disponible
nombre = "programa"
def usar_globales():
# Python busca PI en el frame local → no está
# Python salta al frame global → ✓ encontrado
area = PI * 5 ** 2
print(f"área: {area:.2f}")
print(f"ejecutando desde: {nombre}")
usar_globales()
print("---")
# El frame global sigue ahí después de llamar funciones
x_global = "sigo aquí"
f(10) # crea y destruye un frame
print(x_global) # "sigo aquí" — el frame global no cambió
— Scope enclosing = los frames intermedios del stack —
Cuando una función interior se está ejecutando, el frame de la función exterior sigue en el stack — está esperando. Si la interior busca un nombre y no lo encuentra en su propio frame, Python mira el frame de la exterior — que está justo debajo en el stack. Eso es el scope enclosing.
Es una consecuencia directa
de cómo funciona el stack:
cuando inner se ejecuta,
el frame de outer
todavía está en la pila
— Python lo puede consultar.
def outer():
x = "soy de outer" # vive en el frame de outer
def inner():
# Stack en este momento:
# [inner frame] ← activo
# [outer frame] ← esperando, con x="soy de outer"
# [global frame] ← base
# Python busca x:
# 1. Frame de inner → no está
# 2. Frame de outer → ✓ encontrado
print(f"inner ve: {x}")
inner()
print(f"outer ve: {x}")
outer()
print("---")
# El traceback muestra exactamente los frames del stack
def a():
valor = 10
def b():
# Stack: [b frame] [a frame] [global frame]
resultado = valor * 2 # valor viene del frame de a
return resultado
return b()
print(a()) # 20
— LEGB como recorrido del stack de abajo a arriba —
El stack cuando se ejecuta inner()
Python busca de abajo a arriba: Local → Enclosing → Global → Built-in. Se detiene en el primero donde encuentra el nombre.
Pega el ejemplo de
outer e inner
en
pythontutor.com
y avanza paso a paso.
Observa exactamente lo que describe
el diagrama de arriba:
cuando se ejecuta inner
verás tres frames en el stack —
global, outer e inner.
El frame de outer
está activo y visible.
Cuando inner busca
x y no la encuentra
en su propio frame,
Python la encuentra en el frame
de outer —
que está justo debajo.
Cuando inner termina,
su frame desaparece
y el control vuelve
al frame de outer.
"outer"
y no "inner"
en la última línea:
x = "global"
def outer():
x = "outer"
def inner():
x = "inner"
inner()
print(x) # ¿qué imprime?
outer()
def f(x):
return g(x * 2)
def g(x):
return x + 1 # ← ¿cuántos frames aquí?
resultado = f(5) # ← ¿cuántos frames justo antes de llamar a f?
print(resultado) # ← ¿cuántos frames aquí?
global
funciona — ¿qué hace Python
exactamente cuando encuentra
global x
en una función?
¿A qué frame apunta?
n = 10
def doble(x):
return x * 2
def procesar(valor):
resultado = doble(valor)
return resultado + n
print(procesar(5))
Imprime "outer".
Con la analogía de las hojas:
cuando se llama a
inner(),
Python coge una hoja nueva
y anota x = "inner"
en ella.
Cuando inner()
termina, tira esa hoja.
La hoja de outer
sigue sobre la mesa
con x = "outer"
— nunca fue modificada.
Sin nonlocal,
la asignación en
inner
crea una variable local
en su propia hoja —
no toca la hoja de
outer.
Justo antes de llamar
a f:
1 frame — solo el global.
Dentro de g
en return x + 1:
3 frames —
global en la base,
f en el medio
esperando,
g arriba
ejecutándose.
En print(resultado):
1 frame — solo el global.
f y g
ya terminaron y sus frames
fueron destruidos.
Cuando Python encuentra
global x
en una función,
añade una nota especial
en el frame local:
"cuando veas x,
no busques en este frame —
ve directamente al frame
global de la base del stack".
En lugar de crear
una variable local,
todas las referencias
a x
dentro de esa función
apuntan al frame global.
Leer y escribir
x
es leer y escribir
en la hoja del programa principal.
Paso 1 — nivel global:
se crea el frame global
con n = 10,
doble
y procesar
como objetos en el heap.
Paso 2 — llamada a
procesar(5):
aparece el frame de
procesar
con valor = 5.
Stack: global + procesar.
Paso 3 — llamada a
doble(5):
aparece el frame de
doble
con x = 5.
Stack: global + procesar + doble.
Paso 4 — return de
doble:
el frame de doble
desaparece.
resultado = 10
en el frame de
procesar.
Stack: global + procesar.
Paso 5 — resultado + n:
Python busca n
en el frame de
procesar
— no está —
sube al frame global
— n = 10.
Devuelve 20.
Paso 6 — return de
procesar:
el frame desaparece.
Stack: solo global.
print(20).