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:

  1. Local — ámbito de la función actual
  2. Enclosing — ámbito de la función externa (si existe)
  3. Global — ámbito del módulo
  4. 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:

  1. Existir una función anidada (función dentro de función)
  2. La función interna hacer referencia a variables del ámbito de la función externa
  3. 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 nonlocal con moderación: Solo cuando necesites reasignar variables del ámbito enclosing.
  • Combina closures con functools.partial: Para casos simples de funciones parciales, functools.partial puede 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: