38. Decoradores: Añadir comportamiento a las funciones
Los decoradores (decorators) son una de las características más potentes de Python para escribir código limpio y reutilizable. Te permiten modificar o mejorar el comportamiento de las funciones (functions) sin cambiar su código real. En este capítulo, nos basaremos en tu comprensión de las funciones de primera clase (first-class functions) y las clausuras (closures) del Capítulo 23 para explorar cómo funcionan los decoradores y cómo usarlos de forma efectiva.
38.1) Qué son los decoradores y por qué son útiles
Un decorador (decorator) es una función (function) que toma otra función (function) como entrada y devuelve una versión modificada de esa función. Esto es posible porque, como aprendimos en el Capítulo 23, las funciones en Python son objetos de primera clase (first-class objects): pueden pasarse como argumentos y devolverse desde otras funciones. Los decoradores te permiten “envolver” comportamiento adicional alrededor de funciones existentes, lo que facilita añadir funcionalidad común como logging, temporización, validación o control de acceso sin ensuciar tu lógica principal.
Por qué importan los decoradores
Imagina que tienes varias funciones en tu programa y quieres registrar cuándo se llama a cada una. Sin decoradores, podrías escribir algo como esto:
# Sin decoradores: código de logging duplicado
def calculate_total(prices):
print("Calling calculate_total")
result = sum(prices)
print(f"calculate_total returned: {result}")
return result
def find_average(numbers):
print("Calling find_average")
result = sum(numbers) / len(numbers)
print(f"find_average returned: {result}")
return result
def process_order(order_id):
print("Calling process_order")
result = f"Order {order_id} processed"
print(f"process_order returned: {result}")
return result
# Usando las funciones
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60Este enfoque tiene varios problemas:
- Duplicación de código: las líneas de logging se repiten en cada función
- Mezcla de responsabilidades: el código de logging se mezcla con la lógica de negocio
- Difícil de mantener: si quieres cambiar el formato del logging, debes actualizar cada función
- Fácil de olvidar: las funciones nuevas podrían no incluir logging
Los decoradores resuelven estos problemas permitiéndote separar el comportamiento de logging de tus funciones principales:
# Con decoradores: limpio y mantenible
# (Aprenderemos a crear @log_calls en este capítulo)
@log_calls
def calculate_total(prices):
return sum(prices)
@log_calls
def find_average(numbers):
return sum(numbers) / len(numbers)
@log_calls
def process_order(order_id):
return f"Order {order_id} processed"
# Usar las funciones produce la misma salida
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60¿La diferencia? El comportamiento de logging se define una vez en el decorador @log_calls y se reutiliza en todas partes. Tus funciones principales se mantienen limpias y centradas en su objetivo principal.
Casos de uso comunes para decoradores
Los decoradores son especialmente útiles para:
- Logging: registrar cuándo se llaman las funciones y qué devuelven
- Temporización: medir cuánto tardan las funciones en ejecutarse
- Validación: comprobar que los argumentos de una función cumplen ciertos requisitos
- Caché: almacenar resultados de llamadas costosas para reutilizarlos
- Control de acceso: comprobar permisos antes de permitir la ejecución de una función
- Lógica de reintento: reintentar automáticamente operaciones fallidas
- Comprobación de tipos: validar tipos de argumentos y del valor de retorno
La ventaja clave es que escribes el decorador una vez y puedes aplicarlo a muchas funciones con una sola línea de código.
38.2) Funciones como objetos: la base de los decoradores
Antes de poder entender los decoradores, necesitamos repasar y ampliar el concepto de que las funciones son objetos de primera clase (first-class objects) en Python. Como aprendimos en el Capítulo 23, esto significa que las funciones pueden asignarse a variables, pasarse como argumentos y devolverse desde otras funciones.
Las funciones pueden asignarse a variables
Cuando defines una función, Python crea un objeto función (function object) y lo vincula a un nombre:
def greet(name):
return f"Hello, {name}!"
# El objeto función puede asignarse a otra variable
say_hello = greet
# Ambos nombres se refieren al mismo objeto función
print(greet("Alice")) # Output: Hello, Alice!
print(say_hello("Bob")) # Output: Hello, Bob!Los nombres greet y say_hello se refieren al mismo objeto función. Esto es fundamental para cómo funcionan los decoradores.
Las funciones pueden pasarse como argumentos
Puedes pasar funciones a otras funciones igual que cualquier otro valor:
def apply_twice(func, value):
"""Aplicar una función a un valor dos veces."""
result = func(value)
result = func(result)
return result
def add_five(x):
return x + 5
result = apply_twice(add_five, 10)
print(result) # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)Aquí, apply_twice recibe la función add_five como argumento y la llama dos veces.
Las funciones pueden devolver otras funciones
Una función puede crear y devolver una nueva función:
def make_multiplier(factor):
"""Crear una función que multiplica por un factor específico."""
def multiply(x):
return x * factor
return multiply
times_three = make_multiplier(3)
times_five = make_multiplier(5)
print(times_three(10)) # Output: 30
print(times_five(10)) # Output: 50La función make_multiplier devuelve una nueva función que “recuerda” el valor factor mediante una clausura (como aprendimos en el Capítulo 23).
Envolver funciones: el patrón central del decorador
El patrón de decorador combina estos conceptos: una función que toma una función como entrada, crea una función envoltura que añade comportamiento y devuelve la envoltura:
def simple_wrapper(original_func):
"""Envolver una función con comportamiento adicional."""
def wrapper():
print("Before calling the function")
result = original_func()
print("After calling the function")
return result
return wrapper
def say_hello():
print("Hello!")
return "greeting"
# Envolver manualmente la función
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
print(f"Returned: {return_value}")
# Output: Returned: greetingVeamos qué ocurre:
simple_wrapperrecibesay_hellocomooriginal_func- Crea una nueva función
wrapperque:- Imprime "Before calling the function"
- Llama a
original_func()(que essay_hello) - Imprime "After calling the function"
- Devuelve el resultado
simple_wrapperdevuelve la funciónwrapper- Cuando llamamos a
wrapped_hello(), en realidad estamos llamando awrapper, que llama a lasay_hellooriginal dentro
Este es el patrón central detrás de todos los decoradores.
Manejar funciones con argumentos
La envoltura anterior solo funciona con funciones que no aceptan argumentos. Para que funcione con cualquier función, necesitamos *args y **kwargs:
def flexible_wrapper(original_func):
"""Envolver una función que puede aceptar cualquier argumento."""
def wrapper(*args, **kwargs):
# *args captura argumentos posicionales
# **kwargs captura argumentos con nombre
print("Before calling the function")
result = original_func(*args, **kwargs)
print("After calling the function")
return result
return wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# Envolver manualmente la función
greet = flexible_wrapper(greet)
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hello, Alice!
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hi, Bob!Cómo funcionan *args y **kwargs:
Como aprendimos en el Capítulo 20, *args y **kwargs permiten que las funciones acepten un número variable de argumentos:
*argsrecopila todos los argumentos posicionales en una tupla**kwargsrecopila todos los argumentos con nombre en un diccionario- Cuando llamamos a
original_func(*args, **kwargs), los desempaquetamos de nuevo como argumentos para la función original
Este patrón permite que nuestra envoltura funcione con cualquier función, independientemente de cuántos argumentos acepte.
Pasar a una sintaxis más limpia
Este patrón es la base de los decoradores. La sintaxis de decorador que aprenderemos a continuación es solo una forma más limpia de aplicar este patrón. En lugar de escribir:
greet = flexible_wrapper(greet)Usaremos la sintaxis @:
@flexible_wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"Ambas hacen exactamente lo mismo: la sintaxis @ es solo azúcar sintáctico que hace el código más limpio y legible.
38.3) La sintaxis @decorator: aplicación más limpia
Escribir function_name = decorator(function_name) funciona, pero es verboso y es fácil olvidarlo. Python proporciona la sintaxis @decorator como una forma más limpia de aplicar decoradores.
Usar el símbolo @
En lugar de envolver manualmente una función, puedes colocar @decorator_name en la línea inmediatamente anterior a la definición de la función:
def log_call(func):
"""Decorador que registra llamadas a funciones."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def calculate_total(prices):
return sum(prices)
@log_call
def find_average(numbers):
return sum(numbers) / len(numbers)
# Usar las funciones decoradas
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0La sintaxis @log_call es exactamente equivalente a escribir:
def calculate_total(prices):
return sum(prices)
calculate_total = log_call(calculate_total)Pero la sintaxis @ es mucho más limpia y hace que sea inmediatamente obvio que la función está decorada.
Apilar múltiples decoradores
Puedes aplicar múltiples decoradores a la misma función apilándolos:
import time
def log_call(func):
"""Decorador que registra llamadas a funciones."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
def timer(func):
"""Decorador que mide el tiempo de ejecución de una función."""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start_time
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
@log_call
def process_data(items):
total = sum(items)
return total * 2
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
print(f"Final result: {result}")
# Output: Final result: 30Cuando los decoradores se apilan, se aplican de abajo hacia arriba (primero el más cercano a la función):
@timer # Aplicado en segundo lugar (capa más externa)
@log_call # Aplicado primero (el más cercano a la función)
def process_data(items):
passEsto equivale a:
process_data = timer(log_call(process_data))Orden de aplicación (de abajo hacia arriba):
@log_callenvuelve la función original primero@timerenvuelve el resultado (envuelve la función ya envuelta)
Orden de ejecución (de arriba hacia abajo, de la capa más externa a la más interna):
- comienza la envoltura de
timer(más externa, se ejecuta primero) - comienza la envoltura de
log_call(envoltura interna) - se ejecuta la función original
- finaliza la envoltura de
log_call - finaliza la envoltura de
timer(más externa, termina al final)
Piensa en los decoradores como capas de papel de regalo: los aplicas de dentro hacia fuera, pero cuando desenvuelves (ejecutas), vas de fuera hacia dentro.
Aplicación del decorador:
Flujo de ejecución:
38.4) Ejemplos prácticos de decoradores (Logging, temporización, validación)
Ahora exploremos varios decoradores prácticos que podrías usar en programas reales. Estos ejemplos demuestran patrones comunes y muestran cómo los decoradores resuelven problemas del mundo real.
Ejemplo 1: Decorador de logging mejorado
Un decorador de logging más sofisticado que incluye marcas de tiempo y maneja excepciones:
import time
def log_with_timestamp(func):
"""Decorador que registra llamadas a funciones con marcas de tiempo."""
def wrapper(*args, **kwargs):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Calling {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"[{timestamp}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_with_timestamp
def divide(a, b):
return a / b
@log_with_timestamp
def process_user(user_id):
# Simular el procesamiento
if user_id < 0:
raise ValueError("User ID must be positive")
return f"Processed user {user_id}"
# Probar ejecución correcta
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
print(f"Result: {result}")
# Output: Result: 5.0
# Probar ejecución correcta con validación
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
print(user)
# Output: Processed user 42
# Probar manejo de excepciones
try:
divide(10, 0)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
print("Handled division by zero")
# Output: Handled division by zero
try:
process_user(-5)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
print("Handled invalid user ID")
# Output: Handled invalid user IDEste decorador:
- Añade marcas de tiempo a todos los mensajes de log
- Registra tanto finalizaciones exitosas como excepciones
- Vuelve a lanzar las excepciones después de registrarlas (usando
raisesin argumento) - Usa un bloque
try/exceptpara capturar y registrar cualquier excepción
Ejemplo 2: Decorador de temporización de rendimiento
Un decorador que mide y reporta el tiempo de ejecución de una función:
import time
def measure_time(func):
"""Decorador que mide y reporta el tiempo de ejecución."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# Dar formato al tiempo de forma apropiada
if elapsed < 0.001:
time_str = f"{elapsed * 1000000:.2f} microseconds"
elif elapsed < 1:
time_str = f"{elapsed * 1000:.2f} milliseconds"
else:
time_str = f"{elapsed:.2f} seconds"
print(f"{func.__name__} executed in {time_str}")
return result
return wrapper
@measure_time
def find_primes(limit):
"""Encontrar todos los números primos hasta limit."""
primes = []
for num in range(2, limit):
is_prime = True
for divisor in range(2, int(num ** 0.5) + 1):
if num % divisor == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
@measure_time
def calculate_factorial(n):
"""Calcular el factorial de n."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# Probar las funciones decoradas
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digitsEste decorador da formato automáticamente a la medición de tiempo de forma apropiada (microsegundos, milisegundos o segundos) en función de la duración.
Ejemplo 3: Decorador de validación de entrada
Un decorador que valida los argumentos de una función antes de la ejecución:
def validate_positive(func):
"""Decorador que garantiza que todos los argumentos numéricos sean positivos."""
def wrapper(*args, **kwargs):
# Comprobar argumentos posicionales
for i, arg in enumerate(args):
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(
f"Argument {i} to {func.__name__} must be positive, got {arg}"
)
# Comprobar argumentos con nombre
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(
f"Argument '{key}' to {func.__name__} must be positive, got {value}"
)
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
"""Calcular el área de un rectángulo."""
return width * height
@validate_positive
def calculate_discount(price, discount_percent):
"""Calcular el precio con descuento."""
discount = price * (discount_percent / 100)
return price - discount
# Probar entradas válidas
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
# Probar entradas no válidas
try:
calculate_area(-5, 10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 0 to calculate_area must be positive, got -5
try:
calculate_discount(100, discount_percent=-10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10Este decorador:
- Comprueba todos los argumentos numéricos (tanto posicionales como con nombre)
- Lanza un error descriptivo si alguno no es positivo
- Proporciona mensajes de error claros indicando qué argumento falló la validación
38.5) (Opcional) Decoradores con argumentos
Hasta ahora, todos nuestros decoradores han sido funciones simples que toman una función como entrada. Pero ¿y si quieres configurar el comportamiento de un decorador? Por ejemplo, podrías querer un decorador de reintento donde puedas especificar el número de intentos, o un decorador de logging donde puedas especificar el nivel de log.
Los decoradores con argumentos requieren un nivel extra de anidamiento de funciones. En lugar de que un decorador sea una función que toma una función, pasa a ser una función que toma argumentos y devuelve un decorador.
El patrón: fábricas de decoradores
Un decorador con argumentos es en realidad una fábrica de decoradores: una función que crea y devuelve un decorador. La clave para entender esto es saber qué hace Python con el símbolo @.
El principio clave: Python evalúa @ primero
Python siempre evalúa primero lo que venga después de @, y luego usa el resultado para decorar tu función.
Comparemos:
A) Decorador básico:
Basado en este ejemplo:
def log_call(func):
"""Decorador que registra llamadas a funciones."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def greet(name):
return f"Hello, {name}!"Lo que hace Python:
- Evalúa
@log_call→ Resultado:log_callen sí (el objeto función) - Aplica a
greet:greet = log_call(greet)
B) Fábrica de decoradores:
Basado en este ejemplo:
def repeat(times):
"""Nivel 1: Fábrica - recibe configuración"""
def decorator(func):
"""Nivel 2: Decorador - recibe la función a decorar"""
def wrapper(*args, **kwargs):
"""Nivel 3: Envoltura - se ejecuta cuando se llama a la función decorada"""
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Lo que hace Python:
- Evalúa
@repeat(3)→ Resultado:repeat(3)es llamada, devuelve una función decoradora - Aplica ese decorador a
greet:greet = decorator(greet)
La diferencia: @log_call te da la función en sí, pero @repeat(3) llama a una función (repeat) que devuelve un decorador.
Comprender los tres niveles
Una fábrica de decoradores tiene tres funciones anidadas, cada una con un rol específico:
def repeat(times): # Nivel 1: Fábrica
def decorator(func): # Nivel 2: Decorador
def wrapper(*args, **kwargs): # Nivel 3: Envoltura
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decoratorNivel 1 - Fábrica (repeat):
- Toma: configuración (
times) - Devuelve: una función decoradora
- Se llama: cuando Python evalúa
@repeat(3)
Nivel 2 - Decorador (decorator):
- Toma: la función a decorar (
func) - Devuelve: una función envoltura
- Se llama: inmediatamente después del Nivel 1, como parte de la sintaxis @
Nivel 3 - Envoltura (wrapper):
- Toma: los argumentos de la función cuando se llama (
*args, **kwargs) - Devuelve: el resultado
- Se llama: cada vez que llamas a la función decorada
Ejecución paso a paso
Veamos el rastro de lo que pasa con @repeat(3):
# Lo que escribes:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")Paso 1: Python evalúa repeat(3)
decorator = repeat(3) # La fábrica devuelve un decorador (times=3 queda capturado)Paso 2: Python aplica el decorador a greet
def greet(name):
print(f"Hello, {name}!")
greet = decorator(greet) # El decorador devuelve una envoltura (func=greet queda capturada)Nota: En este punto, greet ahora se refiere a la función envoltura. La greet original queda capturada en func.
Paso 3: Cuando llamas a greet("Alice"), se ejecuta la envoltura
greet("Alice") # En realidad llama a wrapper("Alice")
# wrapper usa los 'times' y 'func' capturados¿Por qué tres niveles?
Cada nivel captura información diferente mediante clausuras:
def repeat(times): # Captura: times
def decorator(func): # Captura: func (y recuerda times)
def wrapper(*args, **kwargs): # Captura: times, func, y recibe args
for i in range(times): # Usa 'times' capturado
result = func(*args, **kwargs) # Usa 'func' y 'args' capturados
return result
return wrapper
return decorator- Nivel 1 captura la configuración (
times) - Nivel 2 captura la función a decorar (
func) - Nivel 3 recibe los argumentos cuando se llama (
args,kwargs)
Sin los tres niveles, no podríamos tener un decorador configurable que recuerde tanto sus ajustes como la función que está decorando.
Ejemplo 1: Un decorador de logging configurable
Aquí tienes un ejemplo práctico de un decorador de logging que acepta configuración:
def log_with_prefix(prefix="LOG"):
"""Fábrica de decoradores que crea un decorador de logging con un prefijo personalizado."""
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{prefix}] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[{prefix}] {func.__name__} returned: {result}")
return result
return wrapper
return decorator
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
return sum(prices)
@log_with_prefix() # Usar el prefijo por defecto
def get_average(numbers):
return sum(numbers) / len(numbers)
# Probar las funciones decoradas
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0Observa que:
@log_with_prefix(prefix="INFO")usa un prefijo personalizado@log_with_prefix()usa el prefijo por defecto "LOG"- Debes incluir paréntesis incluso cuando uses los valores por defecto
Ejemplo 2: Un decorador con múltiples argumentos
Aquí tienes un decorador que valida rangos numéricos:
def validate_range(min_value=None, max_value=None):
"""
Fábrica de decoradores que valida que los argumentos numéricos estén dentro de un rango.
Args:
min_value: Valor mínimo permitido (incluyente)
max_value: Valor máximo permitido (incluyente)
"""
def decorator(func):
def wrapper(*args, **kwargs):
# Comprobar todos los argumentos numéricos
all_args = list(args) + list(kwargs.values())
for arg in all_args:
if isinstance(arg, (int, float)):
if min_value is not None and arg < min_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is below minimum {min_value}"
)
if max_value is not None and arg > max_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is above maximum {max_value}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
"""Calcular el porcentaje."""
return (value / total) * 100
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
"""Calcular la edad a partir del año de nacimiento."""
return current_year - birth_year
# Probar entradas válidas
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
# Probar entradas no válidas
try:
calculate_percentage(150, 100)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_percentage received 150, which is above maximum 100
try:
calculate_age(-5, 2025)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_age received -5, which is below minimum 0Cuándo usar decoradores con argumentos
Usa decoradores con argumentos cuando:
- Necesites configurar el comportamiento del decorador
- El mismo decorador deba funcionar de manera diferente en distintos contextos
- Quieras hacer que los decoradores sean más reutilizables y flexibles
Ejemplos comunes incluyen:
- Decoradores de reintento con intentos y esperas configurables
- Decoradores de logging con niveles o formatos de log configurables
- Decoradores de validación con reglas configurables
- Decoradores de caché con tamaños de caché o tiempos de expiración configurables
- Limitación de tasa con límites configurables
Una nota sobre la complejidad
Los decoradores con argumentos añaden un nivel extra de complejidad. Al escribirlos:
- Usa nombres de parámetros claros y descriptivos
- Proporciona valores por defecto sensatos
- Incluye docstrings que expliquen los parámetros
- Considera si la flexibilidad añadida vale la complejidad
Para casos simples, un decorador sin argumentos suele ser más claro y fácil de entender.
Los decoradores son una herramienta potente para escribir código Python limpio y mantenible. Te permiten separar preocupaciones transversales (como logging, temporización y validación) de tu lógica de negocio principal, haciendo que tu código sea más fácil de leer, probar y modificar. A medida que sigas programando en Python, verás decoradores usados ampliamente en frameworks y librerías, y descubrirás muchas oportunidades de escribir tus propios decoradores para resolver problemas comunes de forma elegante.