Decoradores en Python: qué son, cómo escribirlos y cuándo usarlos

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP