106 - Decoradores con Clases

En Python, los decoradores suelen implementarse como funciones, pero también es posible crear decoradores utilizando clases. Esta aproximación ofrece flexibilidad adicional, especialmente cuando se requiere estado interno o métodos más complejos dentro del decorador.

Para que una clase funcione como decorador, debe implementar el método especial __call__. Esto permite que una instancia de la clase se comporte como una función.

Problema 1

Creación de un decorador mínimo utilizando decorador de clase.

Programa: ejercicio358.py

class MiPrimerDecorador:
    """
    Este decorador imprime un mensaje antes y después
    de ejecutar la función, usando __call__.
    """
    def __init__(self, func):
        self.func = func  # Guardamos la función original

    def __call__(self):
        print("Antes de llamar a la función.")
        self.func()  # Llamamos a la función original
        print("Después de llamar a la función.")

# Ahora, definimos la función y la decoramos usando la clase
@MiPrimerDecorador
def saludar():
    print("¡Hola desde la función saludar!")

# Llamamos a la función decorada
saludar()

@MiPrimerDecorador crea una instancia de la clase, pasando la función saludar al constructor (__init__).

Cuando llamamos a saludar(), se invoca __call__ de la instancia de la clase.

__call__ ejecuta la lógica de envoltura:

Imprime un mensaje antes.
Llama a la función original (self.func()).
Imprime un mensaje después.

El decorador basado en clases logra el mismo objetivo de envolver la función, pero utiliza la estructura de una clase (__init__ y __call__) en lugar de funciones anidadas y closures para gestionar la función original y la lógica adicional. Para este ejemplo sencillo, la versión funcional es más concisa, pero la versión de clase ofrece un camino más claro para añadir estado o métodos adicionales si la complejidad lo requiriera.

Problema 2

Medir el tiempo de ejecución de dos funciones empleando decoradores de clase.

Programa: ejercicio359.py

import random
import time

# Decorador de clase para medir tiempo
class MedirTiempo:
    def __init__(self, func):
        self.func = func  # Guardamos la función original

    def __call__(self, *args, **kwargs):
        inicio = time.time()
        resultado = self.func(*args, **kwargs)
        fin = time.time()
        print(f"Tiempo {self.func.__name__}: {fin - inicio:.4f} segundos")
        return resultado

# Ordenamiento Burbuja
@MedirTiempo
def burbuja(lista):
    n = len(lista)
    for i in range(n - 1):
        for j in range(n - 1 - i):
            if lista[j] > lista[j + 1]:
                lista[j], lista[j + 1] = lista[j + 1], lista[j]
    return lista

# Ordenamiento Quicksort
@MedirTiempo
def quicksort(lista):
    def _quicksort(arr):
        if len(arr) <= 1:
            return arr
        pivote = arr[len(arr) // 2]
        izquierda = [x for x in arr if x < pivote]
        medio = [x for x in arr if x == pivote]
        derecha = [x for x in arr if x > pivote]
        return _quicksort(izquierda) + medio + _quicksort(derecha)

    return _quicksort(lista)

# Generar listas con 10000 elementos aleatorios
lista1 = [random.randint(0, 100000) for _ in range(10000)]
lista2 = [random.randint(0, 100000) for _ in range(10000)]

# Ejecutar
burbuja(lista1)
quicksort(lista2)

Problema 3

Implementar un decorador de clase con argumentos.

Programa: ejercicio360.py

class DecoradorParametrizado:
    def __init__(self, mensaje):
        self.mensaje = mensaje  # Guardamos el parámetro del decorador

    def __call__(self, func):
        # Aquí definimos la función envoltura
        def envoltura(*args, **kwargs):
            print(f"{self.mensaje} - Antes de la función")
            resultado = func(*args, **kwargs)
            print(f"{self.mensaje} - Después de la función")
            return resultado
        return envoltura

# Uso del decorador con parámetro
@DecoradorParametrizado("Depuración")
def principal():
    print("Función principal ejecutándose.")

# Llamada
principal()

Los decoradores basados en clases que aceptan argumentos es más sencillo que con funciones. Recuerda que con funciones necesitábamos un triple anidamiento de funciones. Con clases, solo necesitamos que la función externa del patrón de argumento devuelva una instancia de la clase decoradora:

    def __call__(self, func):
        # Aquí definimos la función envoltura
        def envoltura(*args, **kwargs):
            print(f"{self.mensaje} - Antes de la función")
            resultado = func(*args, **kwargs)
            print(f"{self.mensaje} - Después de la función")
            return resultado
        return envoltura

Problema 4

Creación una función que reciba como parámetro el nombre del jugador, luego tira dos dados y muestra el mensaje que gano si suma 7 y perdió en caso contrario. Crear una clase decoradora que grabe en un archivo que se le pasa a la función decoradora el resultado de la tirada de los dos dados.

Programa: ejercicio361.py

import random

class GuardarEnArchivo:
    """Decorador de clase que guarda la salida de la función en un archivo de texto con jugador, dados y resultado."""
    def __init__(self, nombre_archivo):
        self.nombre_archivo = nombre_archivo  # Parámetro del decorador

    def __call__(self, func):
        def envoltura(*args, **kwargs):
            dado1, dado2 = func(*args, **kwargs)
            total = dado1 + dado2
            resultado = "GANO" if total == 7 else "PERDIO"

            with open(self.nombre_archivo, "a") as f:
                f.write(f"{args[0]} {dado1} {dado2} {resultado}\n")

            print(f"{args[0]} {resultado} - Resultado guardado en {self.nombre_archivo}")
            return dado1, dado2, resultado
        return envoltura

# Función que simula lanzar dos dados
@GuardarEnArchivo("partidas.txt")
def jugar_dados(jugador):
    """Simula lanzar 2 dados y devuelve los valores de cada dado."""
    dado1 = random.randint(1, 6)
    dado2 = random.randint(1, 6)
    print(f"{jugador} lanzó los dados: {dado1} + {dado2} = {dado1 + dado2}")
    return dado1, dado2

# Llamadas de ejemplo
jugar_dados("Diego")
jugar_dados("Carlos")

Ventajas y desventajas de los decoradores basados en clases.

Ventajas:

  • Gestión de estado más clara: Las clases son inherentemente mejores para gestionar el estado. Si tu decorador necesita mantener múltiples piezas de información o un estado complejo entre llamadas, una clase ofrece una estructura más organizada y legible que un closure con funciones.
  • Herencia y polimorfismo: Los decoradores basados en clases pueden aprovechar la herencia, permitiendo construir jerarquías de decoradores o reutilizar lógica de forma más orientada a objetos.
  • Métodos adicionales: Puedes añadir métodos auxiliares a la clase decoradora que no sean __init__ o __call__, lo que puede ser útil para la configuración, la depuración o la exposición de información del estado del decorador.
  • Legibilidad para estado complejo: Para decoradores más complejos, la estructura de una clase puede ser más fácil de entender y mantener que múltiples funciones anidadas.

Desventajas:

  • Más verbosidad: Incluso para decoradores simples, la sintaxis de clase (__init__, __call__) es más verbosa que la de una función.
  • Curva de aprendizaje inicial: Para quienes no están familiarizados con los métodos mágicos como __call__, puede resultar un poco menos intuitivo al principio.
  • No siempre necesarios: Para decoradores que no necesitan mantener estado o que tienen una lógica muy simple, un decorador basado en funciones suele ser más conciso y adecuado.

En resumen, los decoradores basados en clases son una herramienta poderosa cuando la complejidad de la lógica del decorador o la necesidad de gestionar un estado persistente y estructurado lo justifican. Ofrecen una alternativa orientada a objetos a los decoradores funcionales, ampliando tus opciones para la metaprogramación en Python.