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

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 19 min de lectura
Each pattern describes a problem which occurs over and over again in our environment
«Cada patrón describe un problema que aparece una y otra vez en nuestro entorno»
Christopher Alexander · Arquitecto y teórico · 1936 – 2022
Christopher Alexander definió los patrones de diseño en arquitectura: soluciones reutilizables a problemas que se repiten. Su obra inspiró directamente el libro Design Patterns de la Banda de los Cuatro (1994), que trasladó la idea al software. El Decorator es uno de esos patrones clásicos: envolver un objeto para añadirle comportamiento sin modificarlo. Python elevó esta idea al nivel del lenguaje con la sintaxis @decorador, convirtiendo un patrón de diseño en una construcción nativa. Alexander habría apreciado la elegancia: el problema y su solución, en una sola línea.

Un decorador es una función que recibe otra función, le añade comportamiento y devuelve el resultado. La sintaxis @decorador es la forma idiomática de Python para resolver problemas transversales —registro, caché, control de acceso, validación, reintentos— sin mezclar esa lógica con el código de negocio. Es uno de los mecanismos más elegantes del lenguaje y está presente en todo el ecosistema: @property, @staticmethod, @lru_cache, @app.route en Flask, @pytest.mark

⚙️ El mecanismo: funciones que envuelven funciones

Antes de ver la sintaxis @, entendamos qué hace un decorador desde cero:

# Un decorador es simplemente una función que:
# 1. Recibe una función como argumento
# 2. Define una función interna (wrapper) que la envuelve
# 3. Devuelve el wrapper

def mi_primer_decorador(func):
    def wrapper():
        print("→ Antes de ejecutar la función")
        resultado = func()           # llamar a la función original
        print("← Después de ejecutar la función")
        return resultado
    return wrapper                   # devolver el wrapper, no llamarlo

# Aplicar el decorador manualmente — así funciona @
def saludar():
    print("¡Hola!")

saludar = mi_primer_decorador(saludar)   # ← esto es exactamente lo que hace @

saludar()
# → Antes de ejecutar la función
# ¡Hola!
# ← Después de ejecutar la función

# Verificación: saludar ahora es el wrapper
print(type(saludar))           # 
print(saludar.__name__)        # 'wrapper'  ← problema que @wraps resuelve

@ Sintaxis @: azúcar sintáctico sobre una idea simple

# La sintaxis @ es idéntica a: función = decorador(función)
# Pero más legible: el decorador aparece justo encima de la definición

def registrar(func):
    def wrapper(*args, **kwargs):       # *args, **kwargs: acepta cualquier firma
        print(f"[LOG] Llamando a {func.__name__}({args}, {kwargs})")
        resultado = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} devolvió: {resultado}")
        return resultado
    return wrapper

# Sin @ (equivalente manual)
def sumar(a, b):
    return a + b
sumar = registrar(sumar)

# Con @ (forma idiomática — preferida siempre)
@registrar
def multiplicar(a, b):
    return a * b

sumar(3, 4)
# [LOG] Llamando a sumar((3, 4), {})
# [LOG] sumar devolvió: 7

multiplicar(3, 4)
# [LOG] Llamando a multiplicar((3, 4), {})
# [LOG] multiplicar devolvió: 12

# ¿Qué pasa con funciones sin argumentos, con argumentos posicionales
# y con keyword arguments? *args + **kwargs lo cubre todo:
@registrar
def saludar(nombre, formal=False):
    saludo = "Buenos días" if formal else "Hola"
    return f"{saludo}, {nombre}"

saludar("Ana")
saludar("Sr. García", formal=True)
Diagrama conceptual de capas de código donde una función queda envuelta por capas concéntricas que representan decoradores aplicados
Un decorador envuelve una función como las capas de una cebolla: cada capa añade comportamiento antes y/o después, sin modificar el núcleo. Con stacking puedes apilar tantas capas como necesites. Fuente: Pexels (licencia libre).

🏷️ @wraps: preservar la identidad de la función

from functools import wraps

# Sin @wraps — el wrapper oculta la función original
def deco_sin_wraps(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@deco_sin_wraps
def mi_funcion():
    """Hace algo importante."""
    pass

mi_funcion.__name__    # 'wrapper'         ← incorrecto
mi_funcion.__doc__     # None              ← perdimos el docstring
mi_funcion.__module__  # módulo del wrapper, no de mi_funcion

# Con @wraps — preserva __name__, __doc__, __module__, __qualname__, __annotations__
def deco_con_wraps(func):
    @wraps(func)                   # ← esta línea lo soluciona todo
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@deco_con_wraps
def mi_funcion():
    """Hace algo importante."""
    pass

mi_funcion.__name__    # 'mi_funcion'      ✅
mi_funcion.__doc__     # 'Hace algo importante.' ✅

# @wraps también preserva __wrapped__ — acceso a la función original
mi_funcion.__wrapped__    #  — la función sin decorar

# Esto permite "desempaquetar" decoradores en tests:
original = mi_funcion.__wrapped__
original()   # llama directamente sin el wrapper

# Regla: SIEMPRE usa @wraps en cualquier decorador que escribas.
# Es una sola línea y evita bugs difíciles de diagnosticar en producción.
💡 Regla de oro: Todo decorador que escribas debe tener @wraps(func) en el wrapper. Sin él, rompas la introspección, los docstrings, los mensajes de error y herramientas como help(), Sphinx o pytest. Es una línea. No la omitas nunca.

🎛️ Decoradores con argumentos propios

from functools import wraps

# Para que un decorador acepte argumentos necesitas un nivel extra:
# una función que devuelve el decorador (fábrica de decoradores)

# Estructura:
# def decorador_con_args(arg1, arg2):    ← nivel 1: recibe los argumentos
#     def decorador(func):               ← nivel 2: recibe la función
#         @wraps(func)
#         def wrapper(*args, **kwargs):  ← nivel 3: envuelve la llamada
#             ...
#             return func(*args, **kwargs)
#         return wrapper
#     return decorador

# Ejemplo: repetir la función N veces
def repetir(n):
    def decorador(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                resultado = func(*args, **kwargs)
            return resultado
        return wrapper
    return decorador

@repetir(3)
def saludar(nombre):
    print(f"Hola, {nombre}!")

saludar("Ana")
# Hola, Ana!
# Hola, Ana!
# Hola, Ana!

# Ejemplo: validar tipos de argumentos
def validar_tipos(**tipos_esperados):
    """Valida que los kwargs recibidos sean del tipo correcto."""
    def decorador(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for nombre_param, tipo in tipos_esperados.items():
                if nombre_param in kwargs:
                    valor = kwargs[nombre_param]
                    if not isinstance(valor, tipo):
                        raise TypeError(
                            f"{func.__name__}: '{nombre_param}' debe ser "
                            f"{tipo.__name__}, recibido {type(valor).__name__}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorador

@validar_tipos(nombre=str, edad=int)
def registrar_usuario(nombre, edad):
    print(f"Usuario: {nombre}, {edad} años")

registrar_usuario(nombre="Ana", edad=28)     # ✅
registrar_usuario(nombre="Ana", edad="28")   # TypeError: 'edad' debe ser int

# Ejemplo: timeout (usando threading)
import signal

def timeout(segundos):
    def decorador(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            import threading
            resultado = [None]
            excepcion = [None]

            def ejecutar():
                try:
                    resultado[0] = func(*args, **kwargs)
                except Exception as e:
                    excepcion[0] = e

            hilo = threading.Thread(target=ejecutar)
            hilo.daemon = True
            hilo.start()
            hilo.join(segundos)

            if hilo.is_alive():
                raise TimeoutError(f"{func.__name__} superó {segundos}s")
            if excepcion[0]:
                raise excepcion[0]
            return resultado[0]
        return wrapper
    return decorador

@timeout(5)
def operacion_lenta():
    import time; time.sleep(10)

📚 Stacking: apilar varios decoradores

from functools import wraps
import time

# Definir tres decoradores simples
def negrita(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"**{func(*args, **kwargs)}**"
    return wrapper

def cursiva(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"_{func(*args, **kwargs)}_"
    return wrapper

def mayusculas(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

# Apilando decoradores — se aplican de ABAJO a ARRIBA
@negrita
@cursiva
@mayusculas
def mensaje(texto):
    return texto

mensaje("hola")   # '**_HOLA_**'

# Equivalente sin @:
# mensaje = negrita(cursiva(mayusculas(mensaje)))
# Los decoradores se aplican en orden: mayusculas primero, luego cursiva, luego negrita

# Orden importa:
@mayusculas
@negrita
@cursiva
def mensaje2(texto):
    return texto

mensaje2("hola")   # '_**HOLA**_'  ← distinto resultado, distinto orden

# Caso real: decoradores de logging + timing + autenticación
def log_llamada(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[LOG] {func.__name__} llamada")
        return func(*args, **kwargs)
    return wrapper

def medir_tiempo(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        resultado = func(*args, **kwargs)
        fin = time.perf_counter()
        print(f"[TIME] {func.__name__}: {(fin-inicio)*1000:.2f}ms")
        return resultado
    return wrapper

def requiere_auth(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        usuario = kwargs.get("usuario")
        if not usuario or not usuario.get("autenticado"):
            raise PermissionError("Acceso denegado: usuario no autenticado")
        return func(*args, **kwargs)
    return wrapper

# Aplicar los tres: primero se valida auth, luego se mide, luego se registra
@log_llamada
@medir_tiempo
@requiere_auth
def obtener_datos_sensibles(query, usuario=None):
    return f"Datos para: {query}"

# Llamada correcta
obtener_datos_sensibles("SELECT *", usuario={"autenticado": True, "nombre": "Ana"})
# [LOG] obtener_datos_sensibles llamada
# [TIME] obtener_datos_sensibles: 0.01ms

# Sin autenticación
obtener_datos_sensibles("SELECT *", usuario={"autenticado": False})
# PermissionError: Acceso denegado
Infografía del mecanismo de decoradores Python: los tres niveles de una fábrica de decoradores, el orden de aplicación en stacking y el efecto de @wraps
Anatomía de un decorador Python: el nivel de fábrica (argumentos propios), el nivel decorador (recibe la función) y el nivel wrapper (envuelve la llamada). A la derecha, el orden de ejecución al apilar múltiples decoradores: de abajo a arriba en la definición, de arriba a abajo en la ejecución. Infografía: Ciberaula.

🏗️ Decoradores implementados como clase

from functools import wraps

# Una clase puede actuar como decorador si implementa __call__
# Ventaja: puede mantener estado entre llamadas

class Contador:
    """Decorador que cuenta cuántas veces se ha llamado a la función."""

    def __init__(self, func):
        wraps(func)(self)       # preservar metadatos en la instancia
        self.func = func
        self.llamadas = 0

    def __call__(self, *args, **kwargs):
        self.llamadas += 1
        print(f"[#{self.llamadas}] {self.func.__name__}")
        return self.func(*args, **kwargs)

    def resetear(self):
        self.llamadas = 0

@Contador
def procesar(dato):
    return dato.upper()

procesar("hola")    # [#1] procesar → 'HOLA'
procesar("mundo")   # [#2] procesar → 'MUNDO'
procesar("python")  # [#3] procesar → 'PYTHON'
print(procesar.llamadas)    # 3
procesar.resetear()
print(procesar.llamadas)    # 0

# Clase decoradora CON argumentos (necesita __init__ para los args del decorador)
class LimitarLlamadas:
    """Lanza una excepción si la función se llama más de N veces."""

    def __init__(self, max_llamadas):
        self.max_llamadas = max_llamadas

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            wrapper.llamadas += 1
            if wrapper.llamadas > self.max_llamadas:
                raise RuntimeError(
                    f"{func.__name__}: límite de {self.max_llamadas} llamadas superado"
                )
            return func(*args, **kwargs)
        wrapper.llamadas = 0
        return wrapper

@LimitarLlamadas(max_llamadas=3)
def api_call(endpoint):
    return f"GET {endpoint}"

api_call("/users")     # ✅
api_call("/posts")     # ✅
api_call("/data")      # ✅
api_call("/extra")     # RuntimeError: límite de 3 llamadas superado

🔑 @property, @staticmethod y @classmethod

# ── @property ───────────────────────────────────────────────────────────────
# Convierte un método en un atributo de solo lectura (o lectura/escritura)

class Temperatura:
    def __init__(self, celsius):
        self._celsius = celsius         # atributo privado (convención)

    @property
    def celsius(self):
        """Temperatura en grados Celsius."""
        return self._celsius

    @celsius.setter
    def celsius(self, valor):
        if valor < -273.15:
            raise ValueError("Temperatura por debajo del cero absoluto")
        self._celsius = valor

    @celsius.deleter
    def celsius(self):
        del self._celsius

    @property
    def fahrenheit(self):
        """Temperatura en grados Fahrenheit (calculada)."""
        return self._celsius * 9/5 + 32

    @property
    def kelvin(self):
        return self._celsius + 273.15

t = Temperatura(100)
t.celsius        # 100          ← acceso sin ()
t.fahrenheit     # 212.0        ← calculada al vuelo
t.kelvin         # 373.15

t.celsius = 25   # llama al setter
t.celsius = -300 # ValueError: Temperatura por debajo del cero absoluto

# Sin @property habría que escribir t.get_celsius() y t.set_celsius(25)
# @property mantiene la interfaz limpia y permite añadir lógica sin cambiar la API


# ── @staticmethod ────────────────────────────────────────────────────────────
# Método que no recibe self ni cls — pertenece a la clase conceptualmente
# pero no necesita acceder al estado de instancia ni de clase

class Validador:
    @staticmethod
    def es_email_valido(email):
        return "@" in email and "." in email.split("@")[-1]

    @staticmethod
    def es_telefono_valido(telefono):
        return telefono.replace(" ", "").replace("+", "").isdigit()

# Llamada sin instanciar
Validador.es_email_valido("ana@test.com")    # True
Validador.es_email_valido("invalido")        # False

# También funciona desde una instancia (pero no tiene sentido crearlo)
v = Validador()
v.es_email_valido("ana@test.com")            # True


# ── @classmethod ─────────────────────────────────────────────────────────────
# Recibe la CLASE (cls) en lugar de la instancia (self)
# Típico uso: constructores alternativos (factory methods)

class Fecha:
    def __init__(self, año, mes, dia):
        self.año = año
        self.mes = mes
        self.dia = dia

    @classmethod
    def desde_string(cls, cadena):
        """Constructor alternativo: Fecha.desde_string('2026-03-10')"""
        año, mes, dia = map(int, cadena.split("-"))
        return cls(año, mes, dia)   # cls = Fecha (o la subclase si se hereda)

    @classmethod
    def hoy(cls):
        from datetime import date
        d = date.today()
        return cls(d.year, d.month, d.day)

    def __repr__(self):
        return f"Fecha({self.año}, {self.mes:02d}, {self.dia:02d})"

f1 = Fecha(2026, 3, 10)
f2 = Fecha.desde_string("2026-03-10")
f3 = Fecha.hoy()
print(f1, f2, f3)

# Diferencia clave entre @staticmethod y @classmethod:
# @staticmethod: no recibe self ni cls — función de utilidad acoplada a la clase
# @classmethod:  recibe cls — puede crear instancias y funciona con herencia

🎯 Casos de uso reales

import time
import functools
import logging

# ── 1. TIMING — medir tiempo de ejecución ────────────────────────────────────
def timing(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        resultado = func(*args, **kwargs)
        elapsed = time.perf_counter() - inicio
        print(f"[TIMING] {func.__name__}: {elapsed*1000:.3f}ms")
        return resultado
    return wrapper

@timing
def calcular_primos(n):
    """Criba de Eratóstenes hasta n."""
    criba = [True] * (n + 1)
    criba[0] = criba[1] = False
    for i in range(2, int(n**0.5) + 1):
        if criba[i]:
            for j in range(i*i, n+1, i):
                criba[j] = False
    return [i for i, es_primo in enumerate(criba) if es_primo]

calcular_primos(100_000)   # [TIMING] calcular_primos: 23.145ms


# ── 2. RETRY — reintentar en caso de error ───────────────────────────────────
def retry(max_intentos=3, espera=1.0, excepciones=(Exception,)):
    """Reintenta la función hasta max_intentos veces ante excepciones."""
    def decorador(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            ultimo_error = None
            for intento in range(1, max_intentos + 1):
                try:
                    return func(*args, **kwargs)
                except excepciones as e:
                    ultimo_error = e
                    if intento < max_intentos:
                        print(f"[RETRY] {func.__name__}: intento {intento} fallido ({e}). "
                              f"Esperando {espera}s…")
                        time.sleep(espera)
            raise ultimo_error
        return wrapper
    return decorador

import random

@retry(max_intentos=4, espera=0.5, excepciones=(ConnectionError,))
def llamar_api(url):
    if random.random() < 0.7:   # falla el 70% de las veces (simulación)
        raise ConnectionError("Timeout de red")
    return {"status": "ok", "url": url}


# ── 3. CACHÉ SIMPLE — memoización sin lru_cache ──────────────────────────────
def cache_simple(func):
    """Caché de resultados por argumentos (solo para args hashables)."""
    almacen = {}
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        clave = (args, tuple(sorted(kwargs.items())))
        if clave not in almacen:
            almacen[clave] = func(*args, **kwargs)
        else:
            print(f"[CACHE] {func.__name__}{args}: resultado cacheado")
        return almacen[clave]
    wrapper.limpiar = lambda: almacen.clear()
    wrapper.tamaño  = lambda: len(almacen)
    return wrapper

@cache_simple
def fibonacci(n):
    if n <= 1: return n
    return fibonacci(n - 1) + fibonacci(n - 2)

fibonacci(10)   # calcula
fibonacci(10)   # [CACHE] fibonacci(10,): resultado cacheado


# ── 4. VALIDAR ARGUMENTOS ─────────────────────────────────────────────────────
def no_nulos(*nombres_param):
    """Lanza ValueError si alguno de los parámetros nombrados es None o vacío."""
    def decorador(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            import inspect
            sig   = inspect.signature(func)
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()
            for nombre in nombres_param:
                valor = bound.arguments.get(nombre)
                if valor is None or valor == "":
                    raise ValueError(f"{func.__name__}: '{nombre}' no puede ser nulo o vacío")
            return func(*args, **kwargs)
        return wrapper
    return decorador

@no_nulos("nombre", "email")
def crear_usuario(nombre, email, rol="usuario"):
    return {"nombre": nombre, "email": email, "rol": rol}

crear_usuario("Ana", "ana@test.com")       # ✅
crear_usuario("", "ana@test.com")          # ValueError: 'nombre' no puede ser nulo
crear_usuario("Ana", None)                 # ValueError: 'email' no puede ser nulo


# ── 5. LOGGING AUTOMÁTICO ─────────────────────────────────────────────────────
def log_errores(logger=None):
    """Registra automáticamente cualquier excepción lanzada por la función."""
    _logger = logger or logging.getLogger(__name__)
    def decorador(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                _logger.error(
                    f"Error en {func.__name__}(args={args}, kwargs={kwargs}): "
                    f"{type(e).__name__}: {e}",
                    exc_info=True
                )
                raise   # re-lanzar para no silenciar el error
        return wrapper
    return decorador

@log_errores()
def dividir(a, b):
    return a / b

dividir(10, 2)   # 5.0
dividir(10, 0)   # registra el error y relanza ZeroDivisionError
Ficha de referencia de decoradores Python: sintaxis, @wraps, stacking, decoradores con argumentos y decoradores built-in
Referencia rápida de decoradores: estructura de los tres niveles (fábrica, decorador, wrapper), reglas de stacking, decoradores built-in del lenguaje (@property, @staticmethod, @classmethod) y los cinco patrones de uso más frecuentes. Infografía: Ciberaula.

🛠️ Programa completo: sistema de middlewares HTTP

"""
Sistema de middlewares para un mini-servidor HTTP simulado.
Cada middleware es un decorador que añade una capa de comportamiento
a los handlers de ruta: autenticación, logging, validación de payload,
rate limiting y formato de respuesta.
"""

import time
import functools
import hashlib
from collections import defaultdict

# ── DECORADORES / MIDDLEWARES ─────────────────────────────────────────────────

def json_response(func):
    """Envuelve el resultado en un dict estándar {ok, data, timestamp}."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            data = func(*args, **kwargs)
            return {"ok": True, "data": data, "ts": time.time()}
        except Exception as e:
            return {"ok": False, "error": str(e), "ts": time.time()}
    return wrapper


def requiere_token(tokens_validos):
    """Valida que el request incluya un token de la lista permitida."""
    def decorador(func):
        @functools.wraps(func)
        def wrapper(request, *args, **kwargs):
            token = request.get("headers", {}).get("Authorization", "")
            if token not in tokens_validos:
                raise PermissionError(f"Token inválido: {token!r}")
            return func(request, *args, **kwargs)
        return wrapper
    return decorador


def rate_limit(max_req_por_minuto=60):
    """Limita el número de peticiones por IP por minuto."""
    historial = defaultdict(list)   # ip → [timestamps]

    def decorador(func):
        @functools.wraps(func)
        def wrapper(request, *args, **kwargs):
            ip  = request.get("ip", "0.0.0.0")
            now = time.time()
            # Limpiar timestamps de más de 60s
            historial[ip] = [t for t in historial[ip] if now - t < 60]
            if len(historial[ip]) >= max_req_por_minuto:
                raise RuntimeError(f"Rate limit: {ip} superó {max_req_por_minuto} req/min")
            historial[ip].append(now)
            return func(request, *args, **kwargs)
        return wrapper
    return decorador


def validar_body(*campos_requeridos):
    """Verifica que el body del request contiene los campos obligatorios."""
    def decorador(func):
        @functools.wraps(func)
        def wrapper(request, *args, **kwargs):
            body = request.get("body", {})
            faltantes = [c for c in campos_requeridos if c not in body or not body[c]]
            if faltantes:
                raise ValueError(f"Campos requeridos faltantes: {faltantes}")
            return func(request, *args, **kwargs)
        return wrapper
    return decorador


def log_request(func):
    """Registra método, ruta, IP y tiempo de respuesta."""
    @functools.wraps(func)
    def wrapper(request, *args, **kwargs):
        inicio  = time.perf_counter()
        metodo  = request.get("method", "GET")
        ruta    = request.get("path", "/")
        ip      = request.get("ip", "0.0.0.0")
        print(f"  → {metodo} {ruta} desde {ip}")
        resultado = func(request, *args, **kwargs)
        elapsed   = (time.perf_counter() - inicio) * 1000
        estado    = "200 OK" if resultado.get("ok") else "500 Error"
        print(f"  ← {estado} en {elapsed:.2f}ms")
        return resultado
    return wrapper


# ── HANDLERS DE RUTA ─────────────────────────────────────────────────────────

TOKENS_VALIDOS = {"token-ana-2026", "token-admin-secreto"}

@log_request
@json_response
@requiere_token(TOKENS_VALIDOS)
@rate_limit(max_req_por_minuto=10)
def get_usuarios(request):
    """Handler: GET /usuarios"""
    return [
        {"id": 1, "nombre": "Ana",   "rol": "admin"},
        {"id": 2, "nombre": "Luis",  "rol": "usuario"},
        {"id": 3, "nombre": "Marta", "rol": "editor"},
    ]


@log_request
@json_response
@requiere_token(TOKENS_VALIDOS)
@rate_limit(max_req_por_minuto=10)
@validar_body("nombre", "email")
def post_usuario(request):
    """Handler: POST /usuarios"""
    body = request["body"]
    nuevo_id = hashlib.md5(body["email"].encode()).hexdigest()[:8]
    return {"id": nuevo_id, "nombre": body["nombre"], "email": body["email"]}


# ── SIMULACIÓN DE PETICIONES ─────────────────────────────────────────────────

print("\n" + "═" * 55)
print("  SIMULACIÓN DE PETICIONES HTTP")
print("═" * 55)

# Petición válida
req_get = {
    "method": "GET", "path": "/usuarios",
    "ip": "192.168.1.10",
    "headers": {"Authorization": "token-ana-2026"},
}
respuesta = get_usuarios(req_get)
print(f"  Usuarios: {len(respuesta['data'])} encontrados\n")

# Petición POST con body completo
req_post = {
    "method": "POST", "path": "/usuarios",
    "ip": "192.168.1.10",
    "headers": {"Authorization": "token-admin-secreto"},
    "body": {"nombre": "Pedro", "email": "pedro@test.com"},
}
respuesta = post_usuario(req_post)
print(f"  Nuevo usuario creado: {respuesta['data']}\n")

# Petición con token inválido
req_bad_token = {
    "method": "GET", "path": "/usuarios",
    "ip": "10.0.0.1",
    "headers": {"Authorization": "token-falso"},
}
respuesta_error = get_usuarios(req_bad_token)
print(f"  Token inválido: ok={respuesta_error['ok']}, error={respuesta_error['error']}\n")

# Petición sin campos requeridos
req_sin_body = {
    "method": "POST", "path": "/usuarios",
    "ip": "192.168.1.10",
    "headers": {"Authorization": "token-ana-2026"},
    "body": {"nombre": ""},   # falta email, nombre vacío
}
respuesta_error2 = post_usuario(req_sin_body)
print(f"  Body inválido: ok={respuesta_error2['ok']}, error={respuesta_error2['error']}\n")
print("═" * 55)

🐛 Errores clásicos con decoradores

1. Olvidar @wraps — el más frecuente

def mi_deco(func):
    def wrapper(*args, **kwargs):          # ← sin @wraps(func)
        return func(*args, **kwargs)
    return wrapper

@mi_deco
def calcular(x):
    """Calcula algo."""
    return x * 2

calcular.__name__   # 'wrapper'   ← roto: rompe help(), pytest, Sphinx
calcular.__doc__    # None        ← roto: docstring perdido

# ✅ Solución: siempre @functools.wraps(func) en el wrapper

2. Llamar al decorador en vez de pasarlo

@mi_deco()     # ← paréntesis: llama a mi_deco() → error si no devuelve decorador
def funcion():
    pass

@mi_deco       # ✅ sin paréntesis cuando el decorador no acepta argumentos
def funcion():
    pass

# La confusión surge con decoradores que PUEDEN tener argumentos opcionales.
# Si quieres soportar @mi_deco y @mi_deco(arg), necesitas detección:
def mi_deco_flexible(_func=None, *, argumento=None):
    def decorador(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    if _func is not None:       # usado como @mi_deco sin paréntesis
        return decorador(_func)
    return decorador            # usado como @mi_deco(...) con paréntesis

3. El orden del stacking importa — y es contraintuitivo

# Los decoradores se APLICAN de abajo a arriba
# pero se EJECUTAN de arriba a abajo en la llamada

@A
@B
@C
def f(): pass
# Equivalente a: f = A(B(C(f)))
# Al llamar f(): primero ejecuta A.wrapper, que llama B.wrapper, que llama C.wrapper

# Error frecuente con @staticmethod / @classmethod:
class MiClase:
    @staticmethod
    @mi_deco          # ❌ @staticmethod debe ir FUERA, encima
    def metodo(): pass

    @mi_deco          # ✅ @mi_deco encima de @staticmethod
    @staticmethod
    def metodo(): pass

4. Decorar sin mantener el valor de retorno

def deco_roto(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)   # ← no guarda ni devuelve el resultado
    return wrapper

@deco_roto
def sumar(a, b):
    return a + b

sumar(3, 4)    # None  ← el 7 se perdió

# ✅ Siempre: return func(*args, **kwargs)
def deco_correcto(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)    # ← devolver siempre el resultado
    return wrapper

✅ Resumen y próximos pasos

Los decoradores son la herramienta de Python para aplicar comportamiento transversal de forma limpia y reutilizable. La regla esencial es simple: tres niveles si el decorador acepta argumentos, dos si no; y siempre @functools.wraps(func) en el wrapper. Con ese esquema puedes construir sistemas completos de logging, caché, autenticación, validación y reintentos sin contaminar el código de negocio.

La siguiente lección cierra el Módulo 4 con generadores e iteradores: yield, expresiones generadoras y el protocolo de iteración que hace posible procesar secuencias infinitas con memoria constante.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Decoradores en Python: qué son, cómo funcionan y cuándo usarlos

Las dudas más comunes respondidas de forma clara y directa.

Crea una nueva función (el wrapper) y la asigna al mismo nombre. La función original no se toca: sigue existiendo como el argumento func que recibió el decorador, y el wrapper la llama internamente. Lo que cambia es el nombre: después de aplicar @mi_deco a saludar, el nombre saludar apunta al wrapper, no a la función original. Por eso es crucial usar @functools.wraps(func) en el wrapper: sin él, saludar.__name__ devolvería "wrapper" y saludar.__doc__ quedaría vacío, lo que rompe la introspección, la documentación automática y algunas herramientas como pytest.
Son mecanismos para distintos niveles de reutilización. La herencia y los mixins añaden comportamiento a una clase completa: todos los métodos heredan la funcionalidad. Un decorador añade comportamiento a una función o método específico, de forma quirúrgica y sin alterar la estructura de clases. En la práctica, los decoradores son más compositivos: puedes apilar @retry, @cache y @log sobre una sola función sin crear jerarquías de herencia. La elección depende de si el comportamiento extra es transversal a toda la clase (herencia) o específico de ciertas funciones (decorador).
Sí, aunque el impacto es mínimo en la mayoría de casos. Cada llamada a la función decorada pasa por el wrapper, lo que añade una llamada de función extra en el stack. En benchmarks, esto supone entre 50 y 200 nanosegundos por llamada, insignificante en funciones que realizan cualquier trabajo real. El impacto es relevante solo en bucles muy ajustados que llaman a funciones triviales millones de veces. En esos casos, puedes desreferenciar la función antes del bucle: f = mi_funcion_decorada; for x in datos: f(x).
Sí, con un matiz: los métodos de instancia reciben self como primer argumento, los de clase reciben cls, y los estáticos no reciben ninguno implícito. Un decorador que usa *args, **kwargs funciona correctamente en todos los casos porque captura todos los argumentos sin importar su nombre. El orden de apilado con @staticmethod y @classmethod sí importa: @mi_deco debe ir siempre encima de @staticmethod o @classmethod, porque Python procesa los decoradores de abajo a arriba y @staticmethod/@classmethod deben recibir la función cruda, no ya envuelta.
Cuando el decorador necesita mantener estado entre llamadas (contador de invocaciones, caché propio, acumulador de resultados) o cuando quieres que el decorador sea configurable mediante métodos adicionales. Una clase con __call__ puede tener atributos de instancia que persisten entre invocaciones, lo que es más limpio que usar una lista mutable o nonlocal en una closure. También es útil cuando el decorador en sí debe ser introspectable: puedes añadir métodos como reset(), stats() o configure() que el código externo puede llamar sobre la función decorada.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Decoradores en Python: qué son, cómo funcionan y cuándo usarlos? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!