CAP 04 · LEC 05·Funciones

Closures: funciones que recuerdan su entorno

Un closure es una función que mantiene acceso a las variables de su ámbito léxico, incluso cuando ese ámbito ya ha terminado. Son la base de los decoradores, la encapsulación y la memoización.

● AVANZADO12 min lectura5 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

¿Qué es un closure?

Un closure ocurre cuando una función interna hace referencia a variables del ámbito de la función externa que la contiene. La función «cierra sobre» esas variables y las mantiene vivas incluso después de que la función externa ha terminado de ejecutarse.

def crear_saludador(saludo): # 'saludo' es una variable del ámbito de crear_saludador def saludar(nombre): # saludar es un closure: captura 'saludo' del ámbito externo return f"{saludo}, {nombre}!" return saludar # retornamos la función interna # Crear_saludador terminó, pero 'hola_func' todavía tiene acceso a 'saludo' hola_func = crear_saludador("Hola") buen_dia = crear_saludador("Buenos días") print(hola_func("Ana")) # Hola, Ana! print(hola_func("Carlos")) # Hola, Carlos! print(buen_dia("Ana")) # Buenos días, Ana! # Podemos inspeccionar las variables capturadas print(hola_func.__closure__) # (<cell at ...>,) print(hola_func.__closure__[0].cell_contents) # "Hola"
SalidaHola, Ana! Hola, Carlos! Buenos días, Ana!
La «celda» de captura

Python implementa los closures mediante «celdas» (cell objects). Cuando una función interna referencia una variable del ámbito externo, Python crea una celda que ambas funciones comparten. La celda persiste en memoria mientras alguna función la referencia.

La celda de captura

Es crucial entender que los closures capturan la referencia a la variable, no su valor en el momento de la captura. Esto tiene implicaciones importantes:

# ⚠️ Trampa clásica: captura de variable en bucle funciones = [] for i in range(5): def f(): return i # captura la variable i, no su valor actual funciones.append(f) # Cuando ejecutamos, 'i' ya vale 4 (el último valor del bucle) print([f() for f in funciones]) # [4, 4, 4, 4, 4] ← sorpresa # ✅ Solución: capturar el valor con un default argument funciones_correctas = [] for i in range(5): def f(capturado=i): # default arg se evalúa al definir la función return capturado funciones_correctas.append(f) print([f() for f in funciones_correctas]) # [0, 1, 2, 3, 4] ✓ # ✅ Solución alternativa: usar lambda con default lambdas = [lambda x=i: x for i in range(5)] print([l() for l in lambdas]) # [0, 1, 2, 3, 4] ✓
Salida[4, 4, 4, 4, 4] [0, 1, 2, 3, 4] [0, 1, 2, 3, 4]

Casos de uso reales

1. Contador con estado privado (encapsulación)

def crear_contador(inicio=0, paso=1): """Crea un contador con estado encapsulado.""" conteo = [inicio] # usamos lista para poder modificarla con nonlocal-alternativa def incrementar(): conteo[0] += paso return conteo[0] def resetear(): conteo[0] = inicio def valor_actual(): return conteo[0] return incrementar, resetear, valor_actual # El estado 'conteo' es privado — no accesible desde fuera incrementar, resetear, valor = crear_contador(inicio=0, paso=5) print(incrementar()) # 5 print(incrementar()) # 10 print(incrementar()) # 15 print(valor()) # 15 resetear() print(valor()) # 0
Salida5 10 15 15 0

2. Factory de funciones

def crear_validador(minimo: float, maximo: float): """Crea una función que valida si un valor está en el rango.""" def validar(valor: float) -> bool: return minimo <= valor <= maximo validar.__doc__ = f"Valida si el valor está entre {minimo} y {maximo}." return validar # Crear validadores especializados validar_nota = crear_validador(0, 100) validar_porcentaje = crear_validador(0.0, 1.0) validar_temperatura = crear_validador(-273.15, 1_000_000) print(validar_nota(85)) # True print(validar_nota(105)) # False print(validar_porcentaje(0.75)) # True print(validar_temperatura(-300)) # False (bajo cero absoluto)
SalidaTrue False True False

nonlocal — modificar la variable capturada

Por defecto, no puedes reasignar (solo leer) variables del ámbito externo. nonlocal permite modificarlas:

def crear_acumulador(): total = 0 # variable del ámbito externo def agregar(valor): nonlocal total # declarar que queremos modificar 'total' total += valor return total def obtener(): return total # solo lectura, no necesita nonlocal return agregar, obtener agregar, obtener = crear_acumulador() print(agregar(10)) # 10 print(agregar(25)) # 35 print(agregar(5)) # 40 print(obtener()) # 40 # Sin nonlocal, la asignación crea una variable LOCAL (sombrea la externa) def sin_nonlocal(): x = 10 def interna(): # x += 1 # UnboundLocalError: x referenced before assignment x = 99 # ← crea una x LOCAL, no modifica la externa print(x) interna() print(x) # sigue siendo 10 sin_nonlocal()
Salida10 35 40 40 99 10
nonlocal no es global

nonlocal busca la variable en el ámbito inmediatamente externo (la función que contiene). global busca en el módulo. Evita usar globalnonlocal con closures es la alternativa más limpia y encapsulada.

Memoización con closure

Un patrón avanzado y muy útil: usar un closure para cachear resultados de funciones costosas:

def memoizar(func): """Decorador de memoización implementado con closure.""" cache = {} # capturado por el closure def envolvente(*args): if args not in cache: print(f" Calculando {func.__name__}{args}...") cache[args] = func(*args) else: print(f" Cache hit para {args}") return cache[args] envolvente.__name__ = func.__name__ return envolvente @memoizar def fibonacci(n): if n <= 1: return n return fibonacci(n - 1) + fibonacci(n - 2) # Las llamadas se calculan solo una vez print(fibonacci(5)) print() print(fibonacci(6)) # usa los valores ya calculados # También disponible en functools.lru_cache (más eficiente) from functools import lru_cache @lru_cache(maxsize=None) def fibonacci_stdlib(n): if n <= 1: return n return fibonacci_stdlib(n - 1) + fibonacci_stdlib(n - 2)
Salida Calculando fibonacci(5)... Calculando fibonacci(4)... Calculando fibonacci(3)... Calculando fibonacci(2)... Calculando fibonacci(1)... Calculando fibonacci(0)... Cache hit para (1,) 5 Calculando fibonacci(6)... Cache hit para (5,) Cache hit para (4,) 8

Practica

Cinco ejercicios para dominar los closures en Python.