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).