Un decorador es una función que envuelve a otra función para modificar o extender su comportamiento sin tocar su código. Es uno de los patrones más usados en Python para logging, caché, reintentos, control de acceso y medición de rendimiento.
Qué son realmente los decoradores
La sintaxis @decorador es azúcar sintáctico para funcion = decorador(funcion). Un decorador recibe una función, devuelve otra (normalmente que llama a la original) y Python sustituye el nombre original por el resultado:
def mi_decorador(func):
def wrapper(*args, **kwargs):
print(f"Llamando a {func.__name__}")
resultado = func(*args, **kwargs)
print(f"Terminó {func.__name__}")
return resultado
return wrapper
@mi_decorador
def saludar(nombre):
print(f"Hola, {nombre}!")
saludar("Ana")
# Llamando a saludar
# Hola, Ana!
# Terminó saludar
functools.wraps: preservar los metadatos
Sin @functools.wraps, el decorador sobreescribe el __name__ y el __doc__ de la función original. Siempre debes usarlo:
import functools
def mi_decorador(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@mi_decorador
def saludar(nombre):
"""Saluda a alguien."""
print(f"Hola, {nombre}!")
print(saludar.__name__) # 'saludar' (sin @wraps sería 'wrapper')
print(saludar.__doc__) # 'Saluda a alguien.'
Ejemplo real 1: medir tiempos de ejecución
import functools
import time
def cronometro(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
inicio = time.perf_counter()
resultado = func(*args, **kwargs)
fin = time.perf_counter()
print(f"{func.__name__} tardó {fin - inicio:.4f}s")
return resultado
return wrapper
@cronometro
def proceso_lento(n):
time.sleep(n)
return f"Procesado en {n}s"
print(proceso_lento(0.5))
# proceso_lento tardó 0.5001s
# Procesado en 0.5s
Ejemplo real 2: reintentos automáticos
import functools
import time
def reintentar(veces=3, espera=1.0):
def decorador(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for intento in range(1, veces + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if intento == veces:
raise
print(f"Intento {intento} fallido: {e}. Reintentando...")
time.sleep(espera)
return wrapper
return decorador
@reintentar(veces=3, espera=0.5)
def llamada_api(url):
import random
if random.random() < 0.7:
raise ConnectionError("Timeout")
return {"datos": "OK"}
Ejemplo real 3: caché simple
import functools
def cachear(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@cachear
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(35)) # Rápido gracias al caché
Python incluye @functools.lru_cache y @functools.cache para este caso concreto. Escribe tu propio decorador solo cuando necesites personalización que las versiones estándar no ofrecen. El patrón wrapper(*args, **kwargs) más @functools.wraps es la plantilla de referencia.
