Volver a Python Intermedio

Decoradores y Clausuras

Los decoradores son un patrón poderoso para modificar o extender el comportamiento de funciones sin cambiar su código fuente.

Clausuras

Una clausura es una función que recuerda las variables de su ámbito externo:

def crear_multiplicador(n): def multiplicar(x): return x * n # n está "capturada" return multiplicar doble = crear_multiplicador(2) triple = crear_multiplicador(3) print(doble(5)) # 10 print(triple(5)) # 15

Decorador Básico

def registrar_llamadas(func): def wrapper(*args, **kwargs): print(f"Llamando a {func.__name__}...") resultado = func(*args, **kwargs) print(f"{func.__name__} retornó: {resultado}") return resultado return wrapper @registrar_llamadas def sumar(a, b): return a + b sumar(3, 4) # Llamando a sumar... # sumar retornó: 7

Preservar Metadatos de la Función

import functools def registrar_llamadas(func): @functools.wraps(func) # Preserva __name__, __doc__, etc. def wrapper(*args, **kwargs): print(f"Llamando a {func.__name__}...") return func(*args, **kwargs) return wrapper

Decorador con Argumentos

def reintentar(veces=3): def decorador(func): @functools.wraps(func) def wrapper(*args, **kwargs): for intento in range(veces): try: return func(*args, **kwargs) except Exception as e: print(f"Intento {intento+1} falló: {e}") raise RuntimeError(f"Falló después de {veces} intentos") return wrapper return decorador @reintentar(veces=3) def llamada_inestable(): import random if random.random() < 0.7: raise ConnectionError("Error de red") return "¡Éxito!"

Decoradores Basados en Clases

class Temporizador: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.llamadas = 0 def __call__(self, *args, **kwargs): import time self.llamadas += 1 inicio = time.perf_counter() resultado = self.func(*args, **kwargs) fin = time.perf_counter() print(f"{self.func.__name__} tardó {fin-inicio:.4f}s (llamada #{self.llamadas})") return resultado @Temporizador def funcion_lenta(): import time time.sleep(0.1) funcion_lenta() funcion_lenta() print(funcion_lenta.llamadas) # 2

Apilamiento de Decoradores

@registrar_llamadas @reintentar(veces=2) def obtener_usuario(user_id): # Los decoradores se aplican de abajo hacia arriba: # reintentar envuelve la función primero, luego registrar_llamadas pass

Idea clave: Los decoradores son solo azúcar sintáctica para func = decorador(func). Entender esto hace que las pilas de decoradores sean fáciles de razonar.