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.
Creación de un decorador mínimo utilizando decorador de clase.
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.
Medir el tiempo de ejecución de dos funciones empleando decoradores de clase.
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)
Implementar un decorador de clase con argumentos.
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
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.
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")
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.