En Python, podemos aplicar más de un decorador sobre una misma función.
Esto se llama encadenamiento de decoradores y es muy útil cuando queremos aplicar varias mejoras combinadas, como:
Medir el tiempo de ejecución. Registrar llamadas en un log. Validar permisos o datos. Agregar trazas de depuración.
El orden de ejecución cuando apilamos decoradores:
@decorador1
@decorador2
def funcion():
...
Esto equivale a:
funcion = decorador1(decorador2(funcion))
Es decir, el decorador más cercano a la función se aplica primero, pero en tiempo de ejecución la llamada pasa primero por el decorador que quedó más arriba en la pila.
def decorador_a(func):
def envoltura(*args, **kwargs):
print("Entrando en A")
resultado = func(*args, **kwargs)
print("Saliendo de A")
return resultado
return envoltura
def decorador_b(func):
def envoltura(*args, **kwargs):
print("Entrando en B")
resultado = func(*args, **kwargs)
print("Saliendo de B")
return resultado
return envoltura
@decorador_a
@decorador_b
def saludar():
print("Hola mundo")
saludar()
Cuando se ejecuta tenemos como salida:
Entrando en A Entrando en B Hola mundo Saliendo de B Saliendo de A
Primero se aplica decorador_b (el más cercano a la función).
Luego decorador_a envuelve todo.
En tiempo de ejecución, se entra primero en A ? luego en B ? función ? se sale de B ? se sale de A.
Encadenar un decorador de autenticación y otro de llamada.
def autenticar(func):
def envoltura(*args, **kwargs):
print(" Verificando credenciales...")
# Aquí podrías validar usuario/contraseña, token, etc.
return func(*args, **kwargs)
return envoltura
def registrar(func):
def envoltura(*args, **kwargs):
print(f"Llamando a {func.__name__} con args={args}, kwargs={kwargs}")
resultado = func(*args, **kwargs)
print(f"{func.__name__} ejecutada con éxito")
return resultado
return envoltura
@autenticar
@registrar
def transferir(dinero, cuenta_destino):
print(f"Transfiriendo {dinero} a {cuenta_destino}")
return True
# Ejecutar
transferir(100, "Cuenta123")
Flujo de ejecución
Primero se aplica @registrar envuelve la función transferir.
Luego se aplica @autenticar envuelve el resultado anterior.
El orden final al llamar transferir(100, "Cuenta123") es:
Verificando credenciales...
Llamando a transferir con args=(100, 'Cuenta123'), kwargs={}
Transfiriendo 100 a Cuenta123
transferir ejecutada con éxito
Definir dos decoradores con parámetros, uno que permita definir el rango de valores válidos y otro que haga un redondeo.
def validar_rango(min_val, max_val):
def decorador(func):
def envoltura(*args, **kwargs):
for arg in args:
if not (arg >= min_val and arg <= max_val):
raise ValueError(f"Valor {arg} fuera del rango [{min_val}, {max_val}]")
return func(*args, **kwargs)
return envoltura
return decorador
def redondear(decimales=2):
def decorador(func):
def envoltura(*args, **kwargs):
resultado = func(*args, **kwargs)
return round(resultado, decimales)
return envoltura
return decorador
@validar_rango(0, 100) # primero valida que los valores estén en 0–100
@redondear(3) # luego redondea el resultado a 3 decimales
def promedio(a, b):
return (a + b) / 2
# Pruebas
print(promedio(50, 75)) # válido ? 62.5
print(promedio(10, 20)) # válido ? 15.0
print(promedio(150, 20)) # lanza ValueError
El decorador @redondear(3) envuelve primero promedio.
Luego @validar_rango(0, 100) envuelve todo lo anterior.
(El orden de escritura es inverso al orden de ejecución).