W3docs

Closures en Python

Aprende cómo funcionan los closures en Python: funciones anidadas, variables capturadas, nonlocal, errores comunes y casos de uso reales con ejemplos ejecutables.

Un closure es una función anidada que recuerda las variables del ámbito en el que fue definida, incluso después de que la función exterior haya retornado. Los closures permiten adjuntar estado privado a una función sin usar una clase, lo que los convierte en una de las herramientas más elegantes de Python para construir callbacks, fábricas y helpers con estado.

Esta página explica cómo funcionan los closures, las tres condiciones que requieren, los errores comunes y los casos de uso prácticos.

¿Qué es un Closure?

Cuando Python ejecuta una función, crea un ámbito local que desaparece una vez que la función retorna. Normalmente, cualquier variable definida allí desaparece. Un closure es la excepción: si una función interna hace referencia a una variable de una función exterior, Python mantiene esa variable viva en un objeto cell especial, y la función interna lleva una referencia a esas celdas dondequiera que vaya.

El closure más simple es una función fábrica — una función que construye y retorna otra función:

def make_multiplier(factor):
    def multiply(n):
        return n * factor   # 'factor' is captured from the enclosing scope
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15
print(double(10))  # 20

Cada llamada a make_multiplier crea un closure nuevo con su propia copia independiente de factor. double y triple son completamente independientes aunque hayan sido creados por la misma función.

Tres Condiciones para un Closure

Una función es un closure cuando se cumplen las tres condiciones siguientes:

  1. Existe una función anidada — una función definida dentro de otra función.
  2. La función anidada hace referencia a una variable del ámbito exterior — esa variable se denomina variable libre.
  3. La función exterior retorna la función anidada (o la pasa a otro lugar).
def outer():
    message = 'Hello from outer'  # free variable
    def inner():
        print(message)            # inner references it
    return inner                  # outer returns inner

greet = outer()
greet()   # Hello from outer

Tras retornar outer(), su marco local desaparece, pero message sobrevive dentro de greet.__closure__.

Inspeccionar un Closure

Python expone las celdas del closure a través del atributo __closure__:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
print(add5(3))                            # 8
print(add5.__closure__)                   # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents)  # 5

__closure__ es una tupla de objetos cell — uno por cada variable capturada. Si una función no es un closure, __closure__ vale None.

Modificar Variables Capturadas con nonlocal

Por defecto, puedes leer una variable capturada pero no reasignarla. Intentar asignarle un valor crea una nueva variable local, lo cual normalmente no es lo que se desea. Usa la palabra clave nonlocal para indicarle a Python que te refieres a la variable del ámbito exterior:

def make_counter(start=0):
    count = start
    def increment(step=1):
        nonlocal count      # rebind the enclosing 'count', not a new local
        count += step
        return count
    return increment

counter = make_counter()
print(counter())    # 1
print(counter())    # 2
print(counter(5))   # 7

counter2 = make_counter(10)
print(counter2())   # 11
print(counter())    # 8  — counter is unaffected

Cada llamada a make_counter produce una celda count independiente. counter y counter2 no comparten estado.

Para profundizar en cómo Python decide a qué ámbito pertenece una variable, consulta Python Scope.

Error Común: Closures en Bucles

Un error clásico es crear closures dentro de un bucle esperando que cada uno capture el valor actual de la variable del bucle:

# Wrong — all functions capture the same 'i' cell
funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2]  — not [0, 1, 2]

Los tres lambdas comparten una sola celda que contiene la variable del bucle i. En el momento en que se llaman, i ha alcanzado su valor final de 2.

Solución 1: Argumento por defecto (captura por valor)

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)   # default arg is evaluated immediately

print([f() for f in funcs])  # [0, 1, 2]

Solución 2: Función fábrica

def make_func(i):
    def f():
        return i
    return f

funcs = [make_func(i) for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

La función fábrica crea un ámbito nuevo — y por tanto una celda nueva — en cada iteración. Este enfoque es más explícito y legible.

Casos de Uso Prácticos

Aplicación Parcial

Los closures son una alternativa ligera a functools.partial cuando necesitas una versión preconfigurada de una función:

def make_power(exponent):
    def power(base):
        return base ** exponent
    return power

square = make_power(2)
cube   = make_power(3)

print(square(4))  # 16
print(cube(3))    # 27

Memoización Simple

Un closure puede almacenar un diccionario de caché que persiste entre llamadas:

def make_memoized(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@make_memoized
def slow_square(n):
    return n * n

print(slow_square(4))   # 16
print(slow_square(4))   # 16 (served from cache)
print(slow_square(7))   # 49

Este patrón es exactamente cómo funcionan los Decoradores de Python internamente — un decorador no es más que un closure que envuelve otra función.

Configuración de Callbacks

Los closures son muy útiles para construir callbacks que necesitan algo de contexto incorporado:

def make_logger(prefix):
    def log(message):
        print(f'[{prefix}] {message}')
    return log

info  = make_logger('INFO')
error = make_logger('ERROR')

info('Server started')    # [INFO] Server started
error('Disk full')        # [ERROR] Disk full

Closures vs. Clases

Un closure y una clase con un único método suelen resolver el mismo problema. Elige según la complejidad:

SituaciónPreferir
Un dato de estado, un comportamientoClosure
Múltiples métodos o atributos públicosClase
Necesita ser serializado (p. ej. pickle)Clase
Pasar un callback a otra funciónClosure
# Class approach
class Counter:
    def __init__(self, start=0):
        self.count = start
    def increment(self, step=1):
        self.count += step
        return self.count

# Closure approach
def make_counter(start=0):
    count = start
    def increment(step=1):
        nonlocal count
        count += step
        return count
    return increment

Ambos producen un comportamiento idéntico. El closure es más corto; la clase es más descubrible y extensible.

Conclusión

Los closures permiten que una función anidada tenga su propio estado privado al recordar las variables del ámbito donde fue definida. Los puntos clave son:

  • Un closure requiere una función anidada, una variable libre y que la función interna sea retornada o pasada a otro lugar.
  • Usa nonlocal cuando necesites reasignar (no solo leer) una variable capturada.
  • Evita el error con variables de bucle: usa una función fábrica o un argumento por defecto para tomar una instantánea del valor en cada iteración.
  • Los closures son la base de los decoradores y están estrechamente relacionados con las reglas de ámbito de Python.

Práctica

Práctica
Which keyword allows an inner function to rebind a variable from its enclosing scope?
Which keyword allows an inner function to rebind a variable from its enclosing scope?
Práctica
What does the __closure__ attribute return when a function is NOT a closure?
What does the __closure__ attribute return when a function is NOT a closure?
Was this page helpful?