Si alguna vez has escrito una función dentro de otra función en Python y notaste que la función interna "recordaba" las variables de la función externa incluso después de que esta hubiera terminado, has encontrado un closure. Los closures son uno de los conceptos más elegantes y poderosos del lenguaje, fundamentales para entender temas avanzados como decoradores, generadores y programación funcional.
En esta guía completa, aprenderás qué son los closures, cómo funcionan internamente en Python, cuándo y por qué usarlos, junto con ejemplos prácticos que solidificarán tu comprensión. Comenzaremos con lo básico — las reglas de ámbito de Python — hasta llegar a aplicaciones reales que puedes usar hoy en tu código.
¿Qué son las Reglas de Ámbito de Python?
Antes de entender los closures, necesitas dominar cómo Python resuelve los nombres de las variables. Python sigue la regla LEGB (Local, Enclosing, Global, Built-in) para buscar variables. Cuando haces referencia a una variable, el intérprete busca en este orden:
- Local — ámbito de la función actual
- Enclosing — ámbito de la función externa (si existe)
- Global — ámbito del módulo
- Built-in — ámbito de las funciones nativas de Python
Consulta la documentación oficial de Python sobre definición de funciones para más detalles sobre cómo el intérprete gestiona estos ámbitos.
Observa un ejemplo simple que ilustra LEGB:
x = "global"
def externa():
x = "enclosing"
def interna():
x = "local"
print(x)
interna()
externa() # Salida: local
Cada ámbito tiene su propio x. Pero, ¿qué sucede cuando la función interna no define su propia variable?
Funciones Anidadas y Ámbito Léxico
Python permite definir funciones dentro de funciones — esto se llama funciones anidadas. La función interna tiene acceso a las variables de la función externa gracias al ámbito léxico (o estático).
def externa():
mensaje = "¡Hola desde el ámbito enclosing!"
def interna():
print(mensaje)
interna()
externa() # Salida: ¡Hola desde el ámbito enclosing!
La función interna puede acceder a mensaje porque está en el ámbito enclosing de externa. Esto es ámbito léxico en acción: Python determina el ámbito de las variables en tiempo de compilación, no de ejecución.
Para profundizar en las reglas de ámbito, el tutorial Python Scope & the LEGB Rule de Real Python es una excelente referencia complementaria.
¿Qué es un Closure?
Un closure ocurre cuando una función interna "captura" variables del ámbito de la función externa y continúa teniendo acceso a ellas incluso después de que la función externa haya terminado su ejecución. En otras palabras, el closure "recuerda" el entorno donde fue creado.
Para que se forme un closure, deben cumplirse tres condiciones:
- Existir una función anidada (función dentro de función)
- La función interna hacer referencia a variables del ámbito de la función externa
- La función externa retornar la función interna
Aquí está el ejemplo clásico:
def saludo(saludo_inicial):
def saludar(nombre):
return f"{saludo_inicial}, {nombre}!"
return saludar
hola = saludo("Hola")
adios = saludo("Adiós")
print(hola("María")) # Salida: Hola, María!
print(adios("Juan")) # Salida: Adiós, Juan!
Cuando llamamos a saludo("Hola"), la función externa retorna la función saludar. Incluso después de que saludo haya terminado de ejecutarse, la función retornada aún "recuerda" que saludo_inicial es "Hola". Eso es un closure.
Cómo Funcionan los Closures Internamente
Python expone el closure a través del atributo mágico __closure__. Vamos a inspeccionarlo:
print(hola.__closure__)
print(hola.__closure__[0].cell_contents) # Salida: Hola
print(adios.__closure__[0].cell_contents) # Salida: Adiós
__closure__ es una tupla de objetos cell, uno por cada variable capturada. Cada cell almacena el valor actual de la variable. Así es como Python implementa el "entorno" del closure.
Según la especificación de la jerarquía de tipos de Python, los objetos cell se utilizan para almacenar variables de ámbitos enclosing.
Si una función no captura ninguna variable externa, __closure__ será None:
def funcion_simple():
return 42
print(funcion_simple.closure) # Salida: None
La Palabra Clave nonlocal
Por defecto, cuando asignas un valor a una variable dentro de una función anidada, Python la trata como una variable local a esa función. Para modificar una variable del ámbito enclosing, debes usar la palabra clave nonlocal.
def contador():
total = 0
def incrementar():
nonlocal total
total += 1
return total
return incrementar
cont = contador()
print(cont()) # 1
print(cont()) # 2
print(cont()) # 3
Sin nonlocal, recibirías un UnboundLocalError al intentar modificar total. La PEP 3104 introdujo nonlocal en Python 3 para resolver exactamente esta limitación.
Importante: nonlocal no funciona para variables del ámbito global — para eso existe global. nonlocal solo busca en los ámbitos enclosing (funciones externas).
Casos de Uso Prácticos de Closures
Los closures no son solo un concepto académico. Tienen aplicaciones extremadamente prácticas en el desarrollo diario con Python.
1. Fábricas de Funciones
El ejemplo de saludo que vimos es una fábrica de funciones. Puedes crear variaciones de una misma lógica:
def multiplicador(factor):
def multiplicar(numero):
return numero * factor
return multiplicar
doble = multiplicador(2)
triple = multiplicador(3)
print(doble(5)) # 10
print(triple(5)) # 15
Este patrón se usa ampliamente en bibliotecas como Flask, donde puedes configurar comportamientos específicos a través de closures.
2. Contadores y Acumuladores con Estado
Los closures permiten crear funciones con estado privado sin usar clases:
def crear_contador():
contador = 0
def contar():
nonlocal contador
contador += 1
return contador
return contar
visitas = crear_contador()
print(visitas()) # 1
print(visitas()) # 2
print(visitas()) # 3
La variable contador queda encapsulada dentro del closure, inaccesible desde fuera — es como un "atributo privado" de una función.
3. Memorización (Caché de Resultados)
Los closures son excelentes para implementar memorización, un patrón que almacena resultados de llamadas anteriores para evitar recálculos:
def memoizar(funcion):
cache = {}
def ejecutar(*args):
if args not in cache:
cache[args] = funcion(*args)
print(f"Calculando... {args}")
return cache[args]
return ejecutar
@memoizar
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # Cada valor se calcula solo una vez
Para una explicación detallada sobre cómo los closures se relacionan con los decoradores, consulta nuestra guía completa de Decoradores en Python.
4. Callbacks Personalizados
Los closures son perfectos para crear callbacks que llevan contexto adicional:
def configurar_logger(nivel):
def log(mensaje):
print(f"[{nivel.upper()}] {mensaje}")
return log
info = configurar_logger("info")
error = configurar_logger("error")
info("Sistema iniciado") # [INFO] Sistema iniciado
error("Fallo en conexión") # [ERROR] Fallo en conexión
Closures vs Clases: ¿Cuál Usar?
Tanto los closures como las clases permiten crear objetos con estado. La elección depende del contexto:
| Criterio | Closure | Clase |
|---|---|---|
| Complejidad | Baja — ideal para una o dos variables | Alta — ideal para múltiples métodos y atributos |
| Legibilidad | Simple y directo | Más verboso pero más explícito |
| Estado | Variables encapsuladas en el closure | Atributos públicos o privados |
| Reutilización | Limitada (función única) | Herencia y mixins |
Una regla práctica: si necesitas solo un método con estado interno, usa un closure. Si necesitas múltiples métodos o herencia, usa una clase. Complementa tus estudios con nuestra guía sobre Funciones en Python para entender mejor cuándo usar cada enfoque.
Errores Comunes con Closures
Late Binding (Enlace Tardío)
El problema más famoso con closures en Python es el late binding. Considera:
def crear_funciones():
funciones = []
for i in range(5):
def funcion():
return i
funciones.append(funcion)
return funciones
for f in crear_funciones():
print(f()) # 4 4 4 4 4 (¡no 0 1 2 3 4!)
Todas las funciones retornan 4 porque el closure captura la referencia a i, no su valor en el momento de la creación. Cuando las funciones se ejecutan (después del bucle), i ya vale 4.
Solución: Usa un argumento por defecto para capturar el valor inmediatamente:
def crear_funciones():
funciones = []
for i in range(5):
def funcion(valor=i): # Captura i inmediatamente
return valor
funciones.append(funcion)
return funciones
for f in crear_funciones():
print(f()) # 0 1 2 3 4
El FAQ oficial de Python explica este comportamiento en detalle, incluyendo por qué los closures se comportan así.
Variables Mutables en Closures
Ten cuidado al capturar objetos mutables:
def crear_acumulador():
items = []
def agregar(item):
items.append(item)
return items
return agregar
acc = crear_acumulador()
print(acc(1)) # [1]
print(acc(2)) # [1, 2]
Esto funciona, pero estás modificando una lista que pertenece al closure. Para principiantes, este comportamiento puede ser sorprendente. Siempre documenta claramente cuando un closure modifica objetos mutables.
Closures y Decoradores
Los closures son la base de los decoradores en Python. Un decorador es esencialmente un closure que recibe una función y retorna una versión modificada de ella:
def temporizador(funcion):
import time
def ejecutar(*args, **kwargs):
inicio = time.time()
resultado = funcion(*args, **kwargs)
fin = time.time()
print(f"{funcion.__name__} se ejecutó en {fin - inicio:.4f}s")
return resultado
return ejecutar
@temporizador
def trabajo_pesado():
return sum(range(10**6))
trabajo_pesado()
El decorador temporizador es un closure: captura la función original (funcion) y la retorna envuelta con lógica adicional de temporización.
Consideraciones de Rendimiento
Los closures tienen un costo de rendimiento pequeño pero real. El acceso a variables del ámbito enclosing es ligeramente más lento que las variables locales porque Python debe navegar por la cadena de ámbitos. Sin embargo, para el 99% de los casos de uso, esta diferencia es irrelevante.
Según la documentación oficial sobre el modelo de ejecución de Python, la resolución de nombres sigue reglas bien definidas que garantizan consistencia y previsibilidad. Si necesitas rendimiento máximo en bucles internos, prefiere variables locales y evita closures en código críticamente ejecutado.
Consejos Finales y Buenas Prácticas
- Prefiere closures simples: Si un closure necesita más de 2-3 variables del ámbito externo, considera usar una clase.
- Documenta los closures: Explica qué variables se están capturando y por qué.
- Evita closures en bucles: El late binding puede causar errores sutiles. Usa argumentos por defecto para capturar valores.
- Usa
nonlocalcon moderación: Solo cuando necesites reasignar variables del ámbito enclosing. - Combina closures con functools.partial: Para casos simples de funciones parciales,
functools.partialpuede ser más claro que un closure.
Conclusión
Los closures son una herramienta fundamental en el cinturón de utilidades del desarrollador Python. Te permiten crear funciones con estado, fábricas de funciones, decoradores y callbacks personalizados de forma elegante y concisa. Entender los closures profundamente es esencial para dominar temas avanzados como programación asíncrona, metaprogramación y frameworks web.
Ahora que entiendes qué son los closures, cómo funcionan internamente a través de __closure__ y cómo aplicarlos en el día a día, estás listo para escribir código Python más expresivo y eficiente. Practica creando tus propios closures — comienza con una fábrica de funciones simple y evoluciona hacia patrones más complejos como memorización y decoradores.
Referencias útiles para continuar tus estudios: