Decoradores en Python: qué son, cómo funcionan y cuándo usarlos
- Qué es un decorador y qué problema resuelve
- El mecanismo: funciones que envuelven funciones
- Sintaxis @: azúcar sintáctico sobre una idea simple
- @wraps: preservar identidad de la función
- Decoradores con argumentos propios
- Stacking: apilar varios decoradores
- Decoradores implementados como clase
- @property, @staticmethod y @classmethod
- Casos de uso reales: timing, retry, validación, caché
- Programa completo: sistema de middlewares HTTP
- Errores clásicos
- Preguntas frecuentes
@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)
🏷️ @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.
@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
🏗️ 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
🛠️ 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.
💬 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.
Todavía no hay mensajes. ¡Sé el primero en participar!