¿Qué es un Closure?

CÓDIGO SIMPLIFICADO

python
def contador_llamadas(funcion_original):
    contador = 0
    
    def funcion_envoltura():
        nonlocal contador
        contador += 1
        print(f"Llamada número: {contador}")
        return funcion_original()  # ← Ejecuta saludar()
    
    return funcion_envoltura

# Definimos UNA sola función-funcion_original)
def saludar(): return "¡Hola!" # Aplicamos el contador saludar_contador = contador_llamadas(saludar) # Usamos la función print(saludar_contador()) # Llamada 1 print(saludar_contador()) # Llamada 2 print(saludar_contador()) # Llamada 3

🎯 SALIDA:

text
Llamada número: 1
¡Hola!
Llamada número: 2
¡Hola!
Llamada número: 3
¡Hola!

LA CLAVE ESTÁ EN ESTA LÍNEA:

python
return funcion_envoltura  # ← ¡NO lleva paréntesis!

🔍 ¿QUÉ PASA PASO A PASO?

PASO 1: Cuando haces:

python
saludar_contador = contador_llamadas(saludar)
  • contador_llamadas crea contador = 0

  • contador_llamadas retorna la FUNCIÓN funcion_envoltura (sin ejecutarla)

  • saludar_contador ahora ES funcion_envoltura

PASO 2: Cuando ejecutas:

python
saludar_contador()
  • Estás ejecutando funcion_envoltura()

  • funcion_envoltura modifica contador (que sigue vivo)

  • funcion_envoltura ejecuta saludar()

🎪 ANALOGÍA FÁCIL:

Imagina que contador_llamadas es una fábrica de cajas mágicas:

python
def fabrica_cajas(funcion):
    contador = 0  # ← Dentro de la fábrica
    
    def caja_magica():
        nonlocal contador
        contador += 1
        print(f"Usos: {contador}")
        return funcion()
    
    return caja_magica  # ← Te doy la CAJA, no lo que hay dentro

# Recibo la caja mágica
mi_caja = fabrica_cajas(saludar)

# Cada vez que abro la caja:
mi_caja()  # ← La caja RECUERDA cuántas veces se abrió
mi_caja()  # ← Porque el contador vive DENTRO de la caja

🎯 RESPUESTA DIRECTA A TU PREGUNTA:

¿Cómo guarda el valor?

Porque return funcion_envoltura retorna la FUNCIÓN COMPLETA con su "mochila" (closure) donde guarda contador.

No retorna el resultado, retorna la función que puede recordar.

python
# Esto:
saludar_contador = contador_llamadas(saludar)

# Es como decir:
# "saludar_contador es una función que cada vez que la llames:
#   1. Incrementará un contador que recuerda
#   2. Ejecutará saludar()
# "

¡La magia está en que la función lleva consigo su memoria! 

🔍 VERSIÓN AÚN MÁS SIMPLE:

python
def contador_llamadas(funcion_original):
    contador = 0
    
    def funcion_envoltura():
        nonlocal contador
        contador += 1
        print(f"Veces que saludé: {contador}")
        return funcion_original()
    
    return funcion_envoltura

def saludar():
    return "¡Hola!"

# Más simple todavía
mi_saludo = contador_llamadas(saludar)

mi_saludo()  # → Veces que saludé: 1
mi_saludo()  # → Veces que saludé: 2  
mi_saludo()  # → Veces que saludé: 3

🎪 RESUMEN VISUAL:

python
# 1. Creamos el contador
mi_funcion = contador_llamadas(saludar)

# 2. Usamos - ¡el contador RECUERDA!
mi_funcion()  # → Llamada 1
mi_funcion()  # → Llamada 2
mi_funcion()  # → Llamada 3

¡Así de simple! La función mi_funcion recuerda cuántas veces se ha llamado, aunque contador_llamadas ya terminó de ejecutarse. 🎯

ha hora con el decorador

¡Ah, con el decorador! Así queda mucho más limpio:

📝 CÓDIGO CON DECORADOR

python
def contador_llamadas(funcion_original):
    contador = 0
    
    def funcion_envoltura():
        nonlocal contador
        contador += 1
        print(f"Llamada número: {contador}")
        return funcion_original()
    
    return funcion_envoltura

# Aplicamos el decorador
@contador_llamadas
def saludar():
    return "¡Hola!"

# ¡Listo! Ahora saludar YA TIENE el contador
print(saludar())  # Llamada 1
print(saludar())  # Llamada 2
print(saludar())  # Llamada 3

🎯 SALIDA:

text
Llamada número: 1
¡Hola!
Llamada número: 2
¡Hola!
Llamada número: 3
¡Hola!

🔍 ¿QUÉ HACE @contador_llamadas?

El decorador es solo azúcar sintáctica. Esto:

python
@contador_llamadas
def saludar():
    return "¡Hola!"

Es equivalente a esto:

python
def saludar():
    return "¡Hola!"

saludar = contador_llamadas(saludar)  # ← ¡El decorador hace esto por ti!

🎪 VERSIÓN MÁS LIMPIA:

python
def contador_llamadas(func):
    count = 0
    def wrapper():
        nonlocal count
        count += 1
        print(f"📞 Llamada #{count}")
        return func()
    return wrapper

@contador_llamadas
def saludar():
    return "¡Hola mundo!"

# Usamos la función decorada directamente
saludar()  # 📞 Llamada #1
saludar()  # 📞 Llamada #2  
saludar()  # 📞 Llamada #3

🎯 RESULTADO FINAL:

text
📞 Llamada #1
📞 Llamada #2
📞 Llamada #3

¡Así es como se usa en la práctica real!

Un closure es una función que "recuerda" el entorno en el que fue creada, incluso después de que ese entorno haya dejado de existir.

nonlocal permite que una función interna acceda y modifique variables de una función externa (pero no globales). Sin nonlocal, la variable sería de solo lectura.

Tu código como ejemplo:

python
def contador_llamadas(funcion_original):
    contador = 0  # ← Variable local de contador_llamadas
def funcion_envoltura(): nonlocal contador # ← ¡Aquí está el closure!¡Aquí referenciamos la variable externa!
contador += 1 # ← Sin nonlocal, esto causaría error
print(f"📞 Llamada número: {contador}") return funcion_original() return funcion_envoltura # ← Retornamos la función, NO el valor

La función que "recuerda" su entorno

La función funcion_envoltura es el closure - la que recuerda su entorno.

Veámoslo en acción:

python
@contador_llamadas
def saludar():
    return "¡Hola!"

# Uso:
mensaje1 = saludar()  # 📞 Llamada número: 1
mensaje2 = saludar()  # 📞 Llamada número: 2
mensaje3 = saludar()  # 📞 Llamada número: 3

¿Qué está pasando realmente?

  1. Cuando se crea funcion_envoltura, captura (recuerda) la variable contador de su entorno

  2. Aunque contador_llamadas ya terminó de ejecutarsefuncion_envoltura mantiene acceso a contador

  3. Cada vez que llamamos a funcion_envoltura, modifica esa misma variable contador

Sin nonlocal no funcionaría:

python
def contador_llamadas_mal(funcion_original):
    contador = 0
    
    def funcion_envoltura():
        # Sin nonlocal - ¡ERROR!
        contador += 1  # UnboundLocalError
        return funcion_original()
    return funcion_envoltura  # ← Retornamos la función CON su closure
funcion_envoltura() retorna lo que retorne funcion_original():

Resumen clave:

  • nonlocal: Permite modificar variables de funciones externas

  • Closurefuncion_envoltura es el closure - recuerda contador y funcion_original

  • Persistencia: El entorno (variables contador y funcion_original) sobrevive incluso después que contador_llamadas terminó

1. contador_llamadas() (función exterior)

python
def contador_llamadas(funcion_original):
    # ...
    return funcion_envoltura  # ← ¡Esto es lo importante!

Retorna: La función funcion_envoltura (sin ejecutarla)

2. funcion_envoltura() (función interior)

python
def funcion_envoltura():
    # ...
    return funcion_original()  # ← ¡Esto es diferente!

Retorna: El resultado de ejecutar funcion_original()

Ejemplo práctico para entenderlo:

python
def mi_funcion():
    return "Hola mundo"

# contador_llamadas retorna una función
funcion_decorada = contador_llamadas(mi_funcion)
print(type(funcion_decorada))  # <class 'function'>

# Ahora ejecutamos la función retornada
resultado = funcion_decorada()
# 📞 Llamada número: 1
print(resultado)  # "Hola mundo"

Visualización del flujo:

text
contador_llamadas(mi_funcion) 
    ↓
RETORNA: funcion_envoltura (con closure)
    ↓
funcion_envoltura() 
    ↓ 
RETORNA: mi_funcion() → "Hola mundo"

¿Por qué es útil?

Esto permite crear decoradores:

python
@contador_llamadas
def saludar():
    return "¡Hola!"

# Equivale a: saludar = contador_llamadas(saludar)

En resumen:

  • contador_llamadas() retorna una función

  • funcion_envoltura() retorna lo que sea que retorne la función original

¿Qué pasa aquí?

  1. Cuando ejecutas @contador_llamadas, la función contador_llamadas termina y su scope local "debería" destruirse

  2. PERO funcion_envoltura mantiene una "referencia" a las variables que necesita (contador y funcion_original)

  3. Esto es el closure: la función interna "encierra" y recuerda las variables del scope exterior

Ejemplo más simple:

python
def crear_contador():
    cuenta = 0  # Variable local
    
    def incrementar():
        nonlocal cuenta
        cuenta += 1
        return cuenta
    
    return incrementar  # ← Retornamos la función, no el valor

# Uso:
mi_contador = crear_contador()
print(mi_contador())  # 1
print(mi_contador())  # 2
print(mi_contador())  # 3

otro_contador = crear_contador()  # ← ¡Nuevo closure independiente!
print(otro_contador())  # 1 (empieza desde 0 otra vez)

¿Por qué es mágico?

  • crear_contador() ya terminó de ejecutarse

  • cuenta debería haberse destruido (era variable local)

  • PERO mi_contador todavía recuerda cuenta y puede modificarla

Características clave:

  1. Persistencia de estado: Las variables "viven" mientras la función viva

  2. Encapsulación: El estado es privado, no accesible desde fuera

  3. Múltiples instancias: Cada closure es independiente

Sin closure no funcionaría:

python
# Esto NO funcionaría sin closure:
def contador_sin_closure():
    return contador + 1  # ← Error: contador no existe aquí

¿Dónde se usan los closures?

  • Decoradores (como tu ejemplo)

  • Callbacks y event handlers

  • Funciones de fábrica que crean funciones personalizadas

  • Programación funcional

Visualización del Flujo del Closure

Te muestro paso a paso dónde ocurre la "magia" de la persistencia:

📍 PASO 1: Creación del Closure

python
def contador_llamadas(funcion_original):
    contador = 0  # 🎯 ESTA variable es la "mágica"
    
    def funcion_envoltura():
        nonlocal contador  # ← ¡CAPTURA la variable!
        contador += 1
        print(f"📞 Llamada número: {contador}")
        return funcion_original()
    
    return funcion_envoltura  # ← Retorna la función CON su entorno
text
MEMORIA en este momento:
┌─────────────────────────────────┐
│ Función contador_llamadas       │
│ - Variable: contador = 0        │ ← 🎯 ESTA se preserva
└─────────────────────────────────┘
     ↑
     │ Capturada por closure
┌─────────────────────────────────┐
│ Función funcion_envoltura       │
│ - __closure__: [contador]       │ ← ¡Lleva consigo la variable!
└─────────────────────────────────┘

📍 PASO 2: Ejecución y "Finalización"

python
# contador_llamadas TERMINA de ejecutarse
funcion_decorada = contador_llamadas(saludar)
# ↑ ¡Pero la variable contador SOBREVIVE!
text
ANTES:                           DESPUÉS (la magia):
┌─────────────────────────┐     ┌─────────────────────────┐
│ Ámbito contador_llamadas│     │ contador_llamadas YA    │
│ contador = 0            │     │ NO EXISTE en ejecución  │
│ funcion_original = ...  │     │ pero...                 │
└─────────────────────────┘     └─────────────────────────┘
                                         ↑
                                         │ ¡LA VARIABLE SOBREVIVE!
┌─────────────────────────┐     ┌─────────────────────────┐
│ funcion_envoltura       │     │ funcion_decorada        │
│                         │     │ - __closure__:          │
│                         │     │   [cell(contador=0),    │ ← 🎯 ¡AQUÍ ESTÁ LA MAGIA!
│                         │     │    cell(funcion_original)]│
└─────────────────────────┘     └─────────────────────────┘

📍 PASO 3: Las Llamadas Múltiples (¡Donde se ve la magia!)

python
# PRIMERA llamada
resultado1 = funcion_decorada()
# 📞 Llamada número: 1
# ↑ ¡contador ahora vale 1 en el closure!

# SEGUNDA llamada  
resultado2 = funcion_decorada()
# 📞 Llamada número: 2
# ↑ ¡contador ahora vale 2 en el closure!

# TERCERA llamada
resultado3 = funcion_decorada()
# 📞 Llamada número: 3
# ↑ ¡contador ahora vale 3 en el closure!

🎯 EL PUNTO EXACTO DE LA MAGIA:

python
def contador_llamadas(funcion_original):
    contador = 0  # 🎯 ¡ESTA variable SOBREVIVE aquí!
    # ↑ Aunque contador_llamadas TERMINÓ, esta variable
    #   vive en el __closure__ de funcion_envoltura
    
    def funcion_envoltura():
        nonlocal contador  # ← Referencia a la variable "inmortal"
        contador += 1      # ← Modifica la variable que debería haber muerto
        return funcion_original()
    
    return funcion_envoltura  # ← Retorna la función + variable capturada

🔍 Para ver la magia en acción:

python
# Verificar el closure
print(funcion_decorada.__closure__)           # → (<cell at ...>,)
print(funcion_decorada.__closure__[0].cell_contents)  # → 3 (el valor actual!)

# Demostración visual
def visualizar_magia():
    @contador_llamadas
    def ejemplo():
        return "¡Funciono!"
    
    # contador_llamadas YA terminó aquí...
    # ¡Pero el closure mantiene vivo el contador!
    
    ejemplo()  # Llamada 1
    ejemplo()  # Llamada 2  
    ejemplo()  # Llamada 3
    
    print(f"El closure guarda: {ejemplo.__closure__[0].cell_contents}")
    # → El closure guarda: 3

visualizar_magia()

¡La magia está en que contador vive en el __closure__ de la función incluso después de que su función padre haya terminado!

Comentarios

Entradas populares de este blog

Calculadora de edad

Funciones en Python: con y sin paréntesis