UdelaR  ·  Máster en Análisis de Datos

Introducción a Python

Clase 6b — Scope: el ámbito de las variables

Asignatura  Introducción a Python Profesor  Daniel Miles Touya Universidad  UdelaR-Uvigo
Parte I

Qué es el scope y por qué importa

El problema: ¿por qué hay variables que no existen donde las buscas? teoría

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
La consecuencia práctica: cuando ves un 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.
  1. Predice qué imprime este código y en qué línea aparece el error:
    resultado = 0
    
    def calcular():
        valor = 42
        return valor * 2
    
    print("A")
    print(calcular())
    print("B")
    print(valor)
    print("C")
  2. ¿Por qué este código imprime 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()
  3. ¿Cuáles de estas afirmaciones son correctas? Justifica cada respuesta:
    — Una variable definida fuera de una función es visible dentro de ella.
    — Una variable definida dentro de una función es visible fuera.
    — Una variable local existe aunque la función nunca se llame.
    — Dos llamadas a la misma función comparten sus variables locales.
Respuesta orientativa
a

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.

b

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.

c

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.

Qué es el scope y por qué importa teoría

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"
El UnboundLocalError: es una variante del 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.
  1. ¿Por qué el scope hace los programas más manejables? Da un ejemplo concreto de lo que ocurriría sin él en un programa con varias funciones que usan la variable i como contador.
  2. En el ejemplo de código de IA: ¿por qué el primer error es un NameError y el segundo un UnboundLocalError? ¿En qué se diferencian?
  3. En el último bloque, 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?
Respuesta orientativa
a

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.

b

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.

c

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.

La independencia de las funciones es exactamente lo que permite la abstracción de proceso de la Clase 6a: puedes usar sorted() sin preocuparte de que sus variables internas interfieran con las tuyas. El scope local hace posible esa abstracción.
Parte II

Scope local y global

Scope global: variables visibles en todo el fichero teoría

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}")
Por qué ocurre el Caso 3: Python analiza el cuerpo completo de la función antes de ejecutarlo. Cuando ve 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)
  1. Predice qué imprime cada bloque antes de ejecutar:
    # 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)
  2. ¿Por qué este código produce 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))
  3. ¿Qué devuelve 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])
  4. ¿Por qué usar constantes globales como 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?
Respuesta orientativa
a

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ó.

b

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.

c

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.

d

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.

Regla práctica: leer globales es aceptable para constantes. Modificar globales desde funciones es una mala práctica casi siempre — la alternativa correcta es pasar el valor como parámetro y devolver el resultado con return.
Scope local: variables que viven dentro de una función teoría

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

  1. Python analiza el cuerpo completo de test_3 antes de ejecutarlo.
  2. Ve x = "local" — una asignación a x.
  3. Decide: x es local en toda la función — incluso antes de la asignación.
  4. Empieza a ejecutar: print(x) → busca x local → no existe aún → UnboundLocalError.
  5. Python nunca llegó a mirar la global — decidió que era local en el paso 3.

— 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.

  1. Predice qué imprime este código y si produce algún error:
    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}")
  2. ¿Por qué este código produce 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()
  3. Ejecuta los tres casos en Python Tutor. En el Caso 1: ¿en qué frame está 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?
  4. Este código generado por IA tiene un error de scope. Identifícalo sin ejecutar:
    umbral = 50
    
    def clasificar(valor):
        if valor > umbral:
            categoria = "alto"
        print(f"{valor} → {categoria}")
        return categoria
    
    print(clasificar(75))
    print(clasificar(30))
Respuesta orientativa
a

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ó.

b

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)
c

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.

d

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
Error frecuente en IA: definir una variable solo dentro de un 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
  1. Predice qué imprime este código antes de ejecutarlo:
    x = "global"
    
    def fx():
        global x
        x = "modificada"
        print(f"dentro: {x}")
    
    print(f"antes: {x}")
    fx()
    print(f"después: {x}")
  2. ¿Por qué este código no necesita global para funcionar correctamente?
    IVA = 0.21
    
    def calcular_precio(base):
        return base * (1 + IVA)
    
    print(calcular_precio(100))
  3. Reescribe este código eliminando el uso de 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}")
  4. En el ejemplo de 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.
Respuesta orientativa
a

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().

b

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.

c
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.

Patrón general: siempre que veas 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í.
d

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.

La diferencia clave: 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.
Parte III

La regla LEGB

La regla LEGB: el orden en que Python busca los nombres teoría

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])
Nombres que nunca deberías usar como variables: 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.

  1. Para cada uso de 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?
  2. ¿Qué imprime este código? Predice antes de ejecutar y explica por qué:
    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()?
  3. Este código generado por IA tiene un error de nombre. Identifícalo y corrígelo:
    def analizar(datos):
        list = [x * 2 for x in datos]
        resultado = sum(list)
        return sorted(list), resultado
    
    print(analizar([3, 1, 4, 1, 5]))
  4. ¿Por qué el nivel Built-in es el último en buscarse y no el primero? ¿Qué problema habría si Python buscara primero en Built-in y luego en Global?
Respuesta orientativa
a

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".

b

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í.

Scope léxico: en Python el scope se determina por dónde está escrita la función en el código fuente — no por desde dónde se llama. Esto se llama lexical scoping y es la razón por la que LEGB funciona de forma predecible.
c

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]))
d

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.

El scope enclosing: funciones anidadas y 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.

  1. Predice qué imprime este código antes de ejecutarlo. Para cada 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)               # ?
  2. ¿Qué imprime este código? ¿Por qué 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}")
  3. Ejecuta los tres casos (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?
  4. Este código generado por IA usa funciones anidadas. ¿Qué imprime? ¿Hay algún problema de scope?
    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))
Respuesta orientativa
a

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".

Con tres niveles de anidamiento el scope enclosing puede tener múltiples capas. Python sube de nivel en nivel hasta encontrar la variable — primero middle, luego outer, luego global, luego built-in.
b

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.

c

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.

d

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.

Anticipo de 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.
Parte IV

Frame, stack y heap: la mecánica del scope

Frame, stack y heap: cómo gestiona Python la memoria teoría

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")
El traceback de Python ES el stack: cuando Python muestra un error con varias líneas de 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.

  1. ¿Cuántos frames hay en el stack cuando se está ejecutando nivel_3() en el ejemplo del traceback? ¿Cuál está arriba y cuál abajo?
  2. Explica con la analogía de las hojas de papel por qué este código imprime 1 tres veces en lugar de 1, 2, 3:
    def contar():
        n = 0
        n += 1
        print(n)
    
    contar()
    contar()
    contar()
  3. ¿Por qué la lista devuelta por 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?
  4. Pega el ejemplo de 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.
Respuesta orientativa
a

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.

b

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.

c

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.

La regla práctica: si quieres que un objeto sobreviva al final de una función, devuélvelo con return y guárdalo en una variable. Sin return — o sin guardar el resultado — el objeto desaparece con el frame.
d

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.

Cómo el frame explica el scope: todo encaja teoría

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()

↑ Built-in len, print, sum, range... — siempre disponible
↑ Global frame Variables del nivel superior del fichero
↑ Enclosing frame (outer) Variables de la función exterior — esperando
↑ Local frame (inner) ← aquí estamos Variables locales de inner — Python busca aquí primero

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.

  1. Con la analogía de las hojas de papel, explica por qué este código imprime "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()
  2. ¿Cuántos frames hay en el stack en cada momento de este código? Indica el número de frames en cada línea marcada:
    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í?
  3. Explica con el modelo de frames por qué global funciona — ¿qué hace Python exactamente cuando encuentra global x en una función? ¿A qué frame apunta?
  4. Ejecuta este código en Python Tutor y describe en tus propias palabras lo que ves en cada paso:
    n = 10
    
    def doble(x):
        return x * 2
    
    def procesar(valor):
        resultado = doble(valor)
        return resultado + n
    
    print(procesar(5))
Respuesta orientativa
a

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.

b

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.

c

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.

d

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).