¿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)) # 10Decorador 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ónDecoradores 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, AnaDecoradores Ú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 segundos2. 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)) # ValueError5. 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étodoDecorar 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 MiClaseDecorador 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 instanciaDecorador 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.