Decoradores Avanzados en Python: Guía Completa con Ejemplos

👤 Admin 📅 4 de noviembre, 2025 ⏱ 17 min 🏷 Python Avanzado

¿Qué son los Decoradores?

Los decoradores son funciones que modifican el comportamiento de otras funciones o clases sin cambiar su código fuente. Son una forma elegante de extender funcionalidad.

Conceptos Fundamentales

Funciones son Objetos de Primera Clase

# Las funciones pueden asignarse a variables
def saludar(nombre):
    return f"Hola, {nombre}"

mi_funcion = saludar
print(mi_funcion("Ana"))  # Hola, Ana

# Las funciones pueden pasarse como argumentos
def ejecutar_funcion(func, valor):
    return func(valor)

resultado = ejecutar_funcion(saludar, "Luis")
print(resultado)  # Hola, Luis

# Las funciones pueden retornar otras funciones
def crear_multiplicador(n):
    def multiplicar(x):
        return x * n
    return multiplicar

doble = crear_multiplicador(2)
print(doble(5))  # 10

Decorador Simple

def mi_decorador(func):
    def wrapper():
        print("Antes de llamar a la función")
        func()
        print("Después de llamar a la función")
    return wrapper

@mi_decorador
def saludar():
    print("¡Hola!")

saludar()
# Salida:
# Antes de llamar a la función
# ¡Hola!
# Después de llamar a la función

# Equivalente a:
# saludar = mi_decorador(saludar)

Decoradores con Argumentos

def mi_decorador(func):
    def wrapper(*args, **kwargs):
        print(f"Argumentos: {args}, {kwargs}")
        resultado = func(*args, **kwargs)
        print(f"Resultado: {resultado}")
        return resultado
    return wrapper

@mi_decorador
def suma(a, b):
    return a + b

@mi_decorador
def saludar(nombre, saludo="Hola"):
    return f"{saludo}, {nombre}"

print(suma(5, 3))
print(saludar("Ana", saludo="Buenos días"))

Preservar Metadatos con functools.wraps

from functools import wraps

# Sin wraps (pierde metadatos)
def decorador_malo(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Con wraps (preserva metadatos)
def decorador_bueno(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@decorador_malo
def funcion1():
    """Esta es la documentación"""
    pass

@decorador_bueno
def funcion2():
    """Esta es la documentación"""
    pass

print(funcion1.__name__)  # wrapper (perdió el nombre)
print(funcion2.__name__)  # funcion2 (preservó el nombre)
print(funcion2.__doc__)   # Esta es la documentación

Decoradores con Parámetros

def repetir(veces):
    def decorador(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(veces):
                resultado = func(*args, **kwargs)
            return resultado
        return wrapper
    return decorador

@repetir(veces=3)
def saludar(nombre):
    print(f"Hola, {nombre}")

saludar("Ana")
# Hola, Ana
# Hola, Ana
# Hola, Ana

Decoradores Útiles - Ejemplos Prácticos

1. Timer - Medir Tiempo de Ejecución

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = func(*args, **kwargs)
        fin = time.time()
        print(f"{func.__name__} tardó {fin - inicio:.4f} segundos")
        return resultado
    return wrapper

@timer
def procesar_datos():
    time.sleep(1)
    return "Datos procesados"

resultado = procesar_datos()
# procesar_datos tardó 1.0001 segundos

2. Logging - Registrar Llamadas

import logging
from functools import wraps

logging.basicConfig(level=logging.INFO)

def log_llamada(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Llamando a {func.__name__} con args={args}, kwargs={kwargs}")
        resultado = func(*args, **kwargs)
        logging.info(f"{func.__name__} retornó {resultado}")
        return resultado
    return wrapper

@log_llamada
def dividir(a, b):
    return a / b

resultado = dividir(10, 2)

3. Cache/Memoization

from functools import wraps

def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args in cache:
            print(f"Retornando desde caché para {args}")
            return cache[args]
        resultado = func(*args)
        cache[args] = resultado
        return resultado
    return wrapper

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

print(fibonacci(10))  # Mucho más rápido con caché

# Python tiene un decorador de caché incorporado
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci_optimizado(n):
    if n < 2:
        return n
    return fibonacci_optimizado(n-1) + fibonacci_optimizado(n-2)

4. Validación de Argumentos

from functools import wraps

def validar_positivo(func):
    @wraps(func)
    def wrapper(numero):
        if numero < 0:
            raise ValueError("El número debe ser positivo")
        return func(numero)
    return wrapper

@validar_positivo
def calcular_raiz(numero):
    return numero ** 0.5

print(calcular_raiz(16))  # 4.0
# print(calcular_raiz(-4))  # ValueError

5. Retry - Reintentar en Caso de Error

import time
from functools import wraps

def retry(intentos=3, delay=1):
    def decorador(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for intento in range(intentos):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if intento == intentos - 1:
                        raise
                    print(f"Intento {intento + 1} falló: {e}. Reintentando...")
                    time.sleep(delay)
        return wrapper
    return decorador

@retry(intentos=3, delay=2)
def conectar_servidor():
    import random
    if random.random() < 0.7:
        raise ConnectionError("Fallo de conexión")
    return "Conectado exitosamente"

print(conectar_servidor())

6. Rate Limiting - Limitar Frecuencia

import time
from functools import wraps

def rate_limit(llamadas_por_segundo):
    intervalo = 1.0 / llamadas_por_segundo
    ultimo_llamado = [0.0]
    
    def decorador(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            tiempo_transcurrido = time.time() - ultimo_llamado[0]
            espera = intervalo - tiempo_transcurrido
            if espera > 0:
                time.sleep(espera)
            ultimo_llamado[0] = time.time()
            return func(*args, **kwargs)
        return wrapper
    return decorador

@rate_limit(llamadas_por_segundo=2)
def hacer_request(url):
    print(f"Request a {url} en {time.time()}")
    return "Respuesta"

# Solo permite 2 llamadas por segundo
for i in range(5):
    hacer_request(f"https://api.com/{i}")

Múltiples Decoradores

# Aplicar múltiples decoradores
@timer
@log_llamada
@memoize
def calcular_factorial(n):
    if n <= 1:
        return 1
    return n * calcular_factorial(n-1)

# Se aplican de abajo hacia arriba:
# calcular_factorial = timer(log_llamada(memoize(calcular_factorial)))

print(calcular_factorial(5))

Decoradores de Clase

Decorar Métodos

class MiClase:
    @staticmethod
    def metodo_estatico():
        return "Método estático"
    
    @classmethod
    def metodo_clase(cls):
        return f"Método de clase de {cls.__name__}"
    
    @property
    def propiedad(self):
        return "Esto es una propiedad"

obj = MiClase()
print(obj.propiedad)  # Accede como atributo, no como método

Decorar Clases Completas

def agregar_str(cls):
    def __str__(self):
        return f"Instancia de {cls.__name__}"
    cls.__str__ = __str__
    return cls

@agregar_str
class MiClase:
    pass

obj = MiClase()
print(obj)  # Instancia de MiClase

Decorador Singleton

def singleton(cls):
    instancias = {}
    
    @wraps(cls)
    def obtener_instancia(*args, **kwargs):
        if cls not in instancias:
            instancias[cls] = cls(*args, **kwargs)
        return instancias[cls]
    
    return obtener_instancia

@singleton
class BaseDatos:
    def __init__(self):
        print("Creando conexión a base de datos")

# Solo se crea una instancia
db1 = BaseDatos()  # Creando conexión a base de datos
db2 = BaseDatos()  # No imprime nada
print(db1 is db2)  # True - misma instancia

Decorador Context Manager

from contextlib import contextmanager

@contextmanager
def archivo_temporal(nombre):
    print(f"Abriendo {nombre}")
    archivo = open(nombre, 'w')
    try:
        yield archivo
    finally:
        print(f"Cerrando {nombre}")
        archivo.close()

# Usar como context manager
with archivo_temporal('temp.txt') as f:
    f.write("Contenido temporal")

Decoradores con Clases

class ContadorLlamadas:
    def __init__(self, func):
        self.func = func
        self.llamadas = 0
    
    def __call__(self, *args, **kwargs):
        self.llamadas += 1
        print(f"Llamada #{self.llamadas} a {self.func.__name__}")
        return self.func(*args, **kwargs)

@ContadorLlamadas
def saludar(nombre):
    return f"Hola, {nombre}"

print(saludar("Ana"))
print(saludar("Luis"))
print(saludar("María"))

Decorador de Depuración

def debug(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Llamando {func.__name__}({signature})")
        resultado = func(*args, **kwargs)
        print(f"{func.__name__!r} retornó {resultado!r}")
        return resultado
    return wrapper

@debug
def calcular_area(base, altura):
    return (base * altura) / 2

area = calcular_area(10, 5)

Buenas Prácticas

  • Usa @wraps: Siempre para preservar metadatos de la función
  • Documenta: Explica qué hace el decorador y cómo usarlo
  • Maneja excepciones: Decide si el decorador debe capturar o propagar errores
  • Considera el overhead: Los decoradores agregan una capa de abstracción
  • Mantén simple: Si es muy complejo, considera otra solución
  • Testing: Prueba tanto el decorador como las funciones decoradas

Conclusión

Los decoradores son una característica poderosa de Python que permite modificar el comportamiento de funciones y clases de manera elegante. Son ideales para cross-cutting concerns como logging, timing, caching, y validación. Dominar los decoradores te permitirá escribir código más limpio, reutilizable y mantenible.