Python & AI Tutorials Logo
Programación Python

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:

python
# 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: 60

Este enfoque tiene varios problemas:

  1. Duplicación de código: las líneas de logging se repiten en cada función
  2. Mezcla de responsabilidades: el código de logging se mezcla con la lógica de negocio
  3. Difícil de mantener: si quieres cambiar el formato del logging, debes actualizar cada función
  4. 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:

python
# 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:

python
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:

python
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:

python
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: 50

La 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:

python
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: greeting

Veamos qué ocurre:

  1. simple_wrapper recibe say_hello como original_func
  2. Crea una nueva función wrapper que:
    • Imprime "Before calling the function"
    • Llama a original_func() (que es say_hello)
    • Imprime "After calling the function"
    • Devuelve el resultado
  3. simple_wrapper devuelve la función wrapper
  4. Cuando llamamos a wrapped_hello(), en realidad estamos llamando a wrapper, que llama a la say_hello original 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:

python
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:

  • *args recopila todos los argumentos posicionales en una tupla
  • **kwargs recopila 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:

python
greet = flexible_wrapper(greet)

Usaremos la sintaxis @:

python
@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:

python
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.0

La sintaxis @log_call es exactamente equivalente a escribir:

python
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:

python
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: 30

Cuando los decoradores se apilan, se aplican de abajo hacia arriba (primero el más cercano a la función):

python
@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):
    pass

Esto equivale a:

python
process_data = timer(log_call(process_data))

Orden de aplicación (de abajo hacia arriba):

  1. @log_call envuelve la función original primero
  2. @timer envuelve 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):

  1. comienza la envoltura de timer (más externa, se ejecuta primero)
  2. comienza la envoltura de log_call (envoltura interna)
  3. se ejecuta la función original
  4. finaliza la envoltura de log_call
  5. 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:

Función original
process_data

Paso 1: @log_call(decorador inferior)

log_call envuelve el original

Paso 2: @timer(decorador superior)

timer envuelve la envoltura de log_call

Final: timer envuelve log_call envuelve el original

Flujo de ejecución:

Llamar a process_data

1. comienza la envoltura de timer
2. comienza la envoltura de log_call
3. se ejecuta la función original
4. finaliza la envoltura de log_call
5. finaliza la envoltura de timer

Devuelve el resultado

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:

python
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 ID

Este 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 raise sin argumento)
  • Usa un bloque try/except para 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:

python
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 digits

Este 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:

python
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 -10

Este 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:

python
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:

  1. Evalúa @log_call → Resultado: log_call en sí (el objeto función)
  2. Aplica a greet: greet = log_call(greet)

B) Fábrica de decoradores:

Basado en este ejemplo:

python
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:

  1. Evalúa @repeat(3) → Resultado: repeat(3) es llamada, devuelve una función decoradora
  2. 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:

python
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 decorator

Nivel 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):

python
# Lo que escribes:
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

Paso 1: Python evalúa repeat(3)

python
decorator = repeat(3)  # La fábrica devuelve un decorador (times=3 queda capturado)

Paso 2: Python aplica el decorador a greet

python
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

python
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:

python
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:

python
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.0

Observa 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:

python
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 0

Cuá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.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai