Patrón Decorador de Python

La idea central del patrón de decorador es el envolvimiento: crear un objeto envoltorio (decorador) para envolver el objeto original, extendiendo así su funcionalidad sin modificar el objeto original.

Conceptos básicos

El patrón de decorador incluye cuatro roles principales:

  1. Interfaz de Componente (Component): define la interfaz común entre el objeto decorado y el decorador.
  2. Componente Concreto (Concrete Component): el objeto original que debe ser decorado.
  3. Clase base del decorador (Decorator): mantiene una referencia a un objeto componente e implementa la interfaz de componente.
  4. Decorador Concreto (Concrete Decorator): implementa la funcionalidad de decoración específica.


Sintaxis básica de los decoradores

Decorador de funciones

El decorador de funciones es la forma más común de decorador; recibe una función como parámetro y devuelve una nueva función.

Ejemplo

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(Algunas operaciones antes de ejecutar la función)
        result = func(*args, **kwargs)
        print(Algunas operaciones después de ejecutar la función)
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Hello, {name}!")

# Uso de decoradores
say_hello("Alice")

Salida:

函数执行前的一些操作
Hello, Alice!
函数执行后的一些操作

Decoradores con parámetros

Si es necesario pasar parámetros al decorador, se debe anidar otra capa de función:

Ejemplo

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(times):
                print(fEjecución {i+1}:)
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f¡Hola, {name}!)

greet("Bob")

Decorador de clases

Además de los decoradores de funciones, Python también admite decoradores de clases. Los decoradores de clases funcionan implementando el__call__método para trabajar.

Decorador de clases básico

Ejemplo

class TimerDecorator:
    def __init__(self, func):
        self.func = func
   
    def __call__(self, *args, **kwargs):
        import time
        start_time = time.time()
        result = self.func(*args, **kwargs)
        end_time = time.time()
        print(fTiempo de ejecución de la función {self.func.__name__}: {end_time - start_time:.4f} segundos)
        return result

@TimerDecorator
def calculate_sum(n):
    return sum(range(n))

result = calculate_sum(1000000)
print(fResultado del cálculo: {result})

Decorador de clases con parámetros

Ejemplo

class LogDecorator:
    def __init__(self, level="INFO"):
        self.level = level
   
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print(f[{self.level}] Llamando a la función: {func.__name__})
            print(f[{self.level}] Parámetros: args={args}, kwargs={kwargs})
            result = func(*args, **kwargs)
            print(f[{self.level}] Valor de retorno: {result})
            return result
        return wrapper

@LogDecorator(level="DEBUG")
def multiply(a, b):
    return a * b

multiply(5, 3)

Decoradores integrados

Python proporciona algunos decoradores integrados útiles:

@staticmethod y @classmethod

Ejemplo

class Calculator:
    @staticmethod
    def add(x, y):
        return x + y
   
    @classmethod
    def multiply(cls, x, y):
        return x * y

# Usando el método estático
result1 = Calculator.add(5, 3)
print(fResultado del método estático: {result1})

# Usando el método de clase
result2 = Calculator.multiply(5, 3)
print(fResultado del método de clase: {result2})

@property

Ejemplo

class Circle:
    def __init__(self, radius):
        self._radius = radius
   
    @property
    def radius(self):
        return self._radius
   
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError(El radio debe ser positivo)
        self._radius = value
   
    @property
    def area(self):
        return 3.14159 * self._radius ** 2

circle = Circle(5)
print(fRadio: {circle.radius})
print(fÁrea: {circle.area})

circle.radius = 10
print(fNuevo radio: {circle.radius})
print(fNueva área: {circle.area})

Escenarios de aplicación práctica de los decoradores

1. Registro de logs

Ejemplo

def log_execution(func):
    def wrapper(*args, **kwargs):
        print(fIniciando ejecución: {func.__name__})
        try:
            result = func(*args, **kwargs)
            print(fCompletado con éxito: {func.__name__})
            return result
        except Exception as e:
            print(fFallo en la ejecución: {func.__name__}, error: {e})
            raise
    return wrapper

@log_execution
def process_data(data):
    # Simular procesamiento de datos
    if not data:
        raise ValueError(Los datos no pueden estar vacíos)
    return [x * 2 for x in data]

# Prueba
data = [1, 2, 3]
result = process_data(data)
print(fResultado del procesamiento: {result})

2. Verificación de permisos

Ejemplo

def require_login(func):
    def wrapper(user, *args, **kwargs):
        if not user.get('is_authenticated', False):
            raise PermissionError(Usuario no autenticado)
        return func(user, *args, **kwargs)
    return wrapper

@require_login
def view_profile(user):
    return fVer el perfil del usuario {user['username']}

# Prueba
user1 = {'username': 'alice', 'is_authenticated': True}
user2 = {'username': 'bob', 'is_authenticated': False}

print(view_profile(user1))  # Ejecución normal
# print(view_profile(user2)) # lanzará PermissionError

3. Decorador de caché

Ejemplo

def cache_results(func):
    cache = {}
   
    def wrapper(*args):
        if args in cache:
            print(fObtener resultado de la caché: {args})
            return cache[args]
        result = func(*args)
        cache[args] = result
        print(fCalcular y almacenar en caché el resultado: {args} -> {result})
        return result
    return wrapper

@cache_results
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Probar el efecto de la caché
print(fibonacci(5))
print(fibonacci(5))  # Esta vez se obtendrá de la caché

Orden de ejecución de múltiples decoradores

Cuando se usan múltiples decoradores, su orden de ejecución es de abajo hacia arriba:

Ejemplo

def decorator1(func):
    def wrapper():
        print(Decorador 1 - antes)
        func()
        print(Decorador 1 - después)
    return wrapper

def decorator2(func):
    def wrapper():
        print(Decorador 2 - antes)
        func()
        print(Decorador 2 - después)
    return wrapper

@decorator1
@decorator2
def my_function():
    print(Función original)

my_function()

Salida:

装饰器1 - 前
装饰器2 - 前
原始函数
装饰器2 - 后
装饰器1 - 后

Preservar la metainformación de la función

Al usar decoradores, la metainformación de la función original (como el nombre de la función, la cadena de documentación, etc.) se sobrescribe con la función envolvente. Se puede usarfunctools.wrapspara preservar esta información:

Ejemplo

from functools import wraps

def preserve_metadata(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Docstring de la función envolvente"""
        return func(*args, **kwargs)
    return wrapper

@preserve_metadata
def example_function():
    """Este es el docstring de la función original"""
    pass

print(fNombre de la función: {example_function.__name__})
print(fDocstring: {example_function.__doc__})

Ejercicios prácticos

Ejercicio 1: Crear un decorador de monitoreo de rendimiento

Escriba un decorador para monitorear el tiempo de ejecución y el uso de memoria de la función.

Ejemplo

import time
import tracemalloc

def performance_monitor(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Iniciar el seguimiento de memoria
        tracemalloc.start()
       
        # Registrar el tiempo de inicio
        start_time = time.time()
       
        # Ejecutar la función
        result = func(*args, **kwargs)
       
        # Registrar el tiempo de finalización
        end_time = time.time()
       
        # Obtener el uso de memoria
        current, peak = tracemalloc.get_traced_memory()
        tracemalloc.stop()
       
        print(fInforme de rendimiento de la función {func.__name__}:)
        print(fTiempo de ejecución: {end_time - start_time:.4f} segundos)
        print(fUso de memoria: actual {current/1024:.2f} KB, pico {peak/1024:.2f} KB)
       
        return result
    return wrapper

@performance_monitor
def process_large_data():
    Simular el procesamiento de grandes cantidades de datos
    data = [i**2 for i in range(100000)]
    return sum(data)

process_large_data()

Ejercicio 2: Implementar un decorador de mecanismo de reintento

Cree un decorador que reintente automáticamente un número específico de veces cuando la función falla.

Ejemplo

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        print(fLa función {func.__name__} falló al ejecutarse, se alcanzó el número máximo de reintentos)
                        raise
                    print(fLa función {func.__name__} falló en el intento {attempts}: {e})
                    print(fReintentando en {delay} segundos...)
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unstable_operation():
    Simular una operación inestable
    import random
    if random.random() < 0.7:  # 70% de probabilidad de fallo
        raise ValueError(Fallo aleatorio)
    return Operación exitosa

# Probar el mecanismo de reintentos
result = unstable_operation()
print(fResultado final: {result})

Resumen

El decorador de Python es una herramienta potente y flexible que ofrece las siguientes ventajas:

  1. Reutilización de código: separar las preocupaciones transversales (como registros, caché, validación) de la lógica de negocio.
  2. Extensión dinámica: agregar nuevas funcionalidades sin modificar el código original.
  3. Mantener la simplicidad: el uso de@la sintaxis hace que el código sea más claro y legible.
  4. Cumple con el principio abierto/cerrado: abierto a la extensión, cerrado a la modificación.

Mejores prácticas para usar decoradores:

  • Usofunctools.wrapsConservar los metadatos de la función
  • Mantener la responsabilidad única del decorador
  • Manejar adecuadamente las excepciones en los decoradores
  • Tener en cuenta el orden de ejecución de los decoradores
Otras extensiones