Skip to content

📙 Clase 10 — Scope (alcance de variables)

Fase 2 · Funciones y código organizado ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Variables locales vs globales y la palabra global.
  • Por qué leer una global funciona pero modificarla lanza UnboundLocalError.
  • El gotcha del lambda en bucles (¡el truco lambda t=tarea de tu To-Do explicado!).

📖 PARTE TEÓRICA

🌍 1. Local vs global

Cada función tiene su propio espacio de variables (scope). Lo que se crea dentro, muere al terminar la función; lo de afuera (global) se puede leer desde dentro.

python
mensaje = "global"          # variable GLOBAL (nivel del archivo)

def leer():
    return mensaje          # leer una global: OK sin nada especial

def crear():
    interna = "local"       # variable LOCAL: solo existe aquí dentro
    return interna

print(leer())     # global
# print(interna)  # 💥 NameError: no existe fuera de la función
┌─ scope GLOBAL ────────────────────────────┐
│ mensaje = "global"                        │
│   ┌─ scope de crear() ──────┐             │
│   │ interna = "local"       │  ← nace y muere con la llamada
│   └─────────────────────────┘             │
└───────────────────────────────────────────┘

✍️ 2. Modificar una global: el UnboundLocalError

Aquí está la trampa: leer una global funciona, pero asignarle dentro de una función hace que Python la trate como local… y explota si intentas usarla antes.

python
contador = 0

def intenta_modificar():
    contador += 1     # 💥 UnboundLocalError: cannot access local variable
                      # 'contador' where it is not associated with a value

¿Por qué? Al ver contador = ... dentro de la función, Python decide que contador es local… pero para hacer += 1 necesita leerla antes de crearla. Solución: declarar global.

python
def modifica_bien():
    global contador       # "me refiero a la de AFUERA"
    contador += 1

modifica_bien(); modifica_bien()
print(contador)           # 2

⚠️ global funciona, pero abusar de él es mala señal: muchas funciones tocando globales = código impredecible. Lo sano: recibir por parámetro y devolver con return (Clase 08). En la Fase 3 verás la solución elegante definitiva: guardar estado en self dentro de una clase.

🕰️ 3. Closures: funciones que recuerdan

Una función definida dentro de otra recuerda las variables de su entorno. Es lo que hace funcionar los command= de tus botones.

python
def crear_contador():
    total = 0
    def incrementar():
        nonlocal total    # como 'global' pero para el scope de la función de afuera
        total += 1
        return total
    return incrementar

contar = crear_contador()
contar(); contar()        # la función "recuerda" su total entre llamadas

💣 4. El gotcha estrella: lambda en un bucle

Este es el bug que ya te tocó en el Ejercicio 2. Un lambda creado en un bucle no congela el valor de la variable: la busca cuando se ejecuta (y para entonces el bucle terminó).

python
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])       # [2, 2, 2]   ← ¡todas ven el ÚLTIMO i!

# La solución: capturar el valor ACTUAL como default (Clase 09):
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])       # [0, 1, 2]   ← cada una congeló su valor

¿Te suena? Es exactamente el truco de tu To-Do:

python
for tarea in tareas:
    boton = ctk.CTkButton(fila, text="X",
                          command=lambda t=tarea: borrar(t))
    #                              ^^^^^^^ sin esto, TODOS los botones
    #                              borrarían la ÚLTIMA tarea del bucle

🧪 Tip de entrevista: "¿Por qué todos los lambdas de un bucle devuelven el último valor?" → Porque capturan la variable, no su valor del momento (late binding). Se arregla con un parámetro por defecto (lambda i=i: ...), que se evalúa al crear el lambda.


🖥️ EN TU APP DE ESCRITORIO

El scope explica dos cosas que ya viviste:

1) Por qué tu app "funciona con globales" (y por qué picará más adelante):

python
tareas = []                # global: los callbacks la leen/modifican

def agregar():
    tareas.append(entry.get())   # OJO: esto FUNCIONA sin 'global'…
    #                              …porque .append() MODIFICA el objeto,
    #                              no reasigna la variable (no hay 'tareas =')

def vaciar():
    global tareas
    tareas = []            # aquí SÍ reasigno → necesito 'global'

💡 Matiz fino: lista.append(...) muta el objeto (no necesita global); lista = [] reasigna la variable (sí lo necesita). Es la diferencia mutación vs reasignación — prima hermana del aliasing de la Clase 01.

2) Por qué cada botón de una lista necesita lambda t=tarea: (sección 4). Sin el default, todos los botones comparten la variable del bucle.


🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — ¿Local o global?

Sin ejecutarlo, predice qué imprime; luego compruébalo.

python
x = 10
def f():
    x = 5
    return x
print(f(), x)
Ver solución
python
# Imprime: 5 10
# El 'x = 5' crea una variable LOCAL distinta; la global x=10 no se toca.

Ejercicio 2 — Arregla el contador

Este código explota. Arréglalo de dos formas: con global y (mejor) con parámetro + return.

python
total = 0
def sumar(n):
    total += n
Ver solución
python
# Forma 1: global (funciona, pero acopla)
total = 0
def sumar(n):
    global total
    total += n

# Forma 2: parámetro + return (recomendada)
def sumar(total, n):
    return total + n

total = 0
total = sumar(total, 5)
print(total)   # 5

Ejercicio 3 — Botones que sí funcionan

Simula el bug de los botones: crea una lista de 3 lambdas que devuelvan 0, 1, 2 (no 2,2,2).

Ver solución
python
botones = [lambda i=i: f"borrar tarea {i}" for i in range(3)]
print([b() for b in botones])
# ['borrar tarea 0', 'borrar tarea 1', 'borrar tarea 2']

❓ Preguntas y respuestas (autoevaluación)

1. ¿Puedes leer una variable global dentro de una función sin declarar nada?

Sí. Lo que no puedes es reasignarla sin global (te da UnboundLocalError).

2. ¿Por qué tareas.append(x) funciona en un callback sin global tareas?

Porque muta el objeto existente; no reasigna la variable. global solo hace falta al reasignar (tareas = []).

3. ¿Qué imprime [f() for f in [lambda: i for i in range(3)]] y por qué?

[2, 2, 2]: los lambdas capturan la variable i (late binding), no su valor de cada vuelta.

4. ¿Cómo se congela el valor de la vuelta en un lambda?

Con un default: lambda i=i: ... — el default se evalúa al crear el lambda.


📎 Apuntes relacionados

➡️ Siguiente

Clase 11 · Módulos e imports — partir tu app en archivos.