Decoradores en Python
🗺️ Mapa de la Lección
⏱ 1.5 horas🔵 Avanzado🎯 Qué aprenderás
- Entender qué son los decoradores y cómo funcionan
- Crear decoradores simples y con parámetros
- Usar @wraps para preservar metadatos
- Aplicar decoradores a funciones y clases
- Implementar decoradores útiles (timing, cache, retry)
🌍 Para qué te sirve
- Medir tiempo de ejecución de funciones
- Agregar logging automático
- Validar argumentos de funciones
- Cachear resultados de funciones costosas
- Controlar acceso (autenticación/autorización)
- Reintentar operaciones que pueden fallar
🎯 ¿Por qué aprender decoradores?
Imagina que tienes 10 funciones y quieres medir cuánto tiempo tarda cada una en ejecutarse. Podrías agregar código de medición en cada función, pero eso es repetitivo y hace el código más difícil de leer.
Los decoradores te permiten:
- Agregar funcionalidad sin modificar código original
- Reutilizar código: escribes una vez, aplicas a muchas funciones
- Separar preocupaciones: la función hace su trabajo, el decorador agrega extras
- Código más limpio: funcionalidad adicional en un solo lugar
- Patrón común: usado extensivamente en frameworks como Flask, Django
🌍 Casos reales donde se usa
Los decoradores están en todos los frameworks y proyectos profesionales:
- Medir tiempo de ejecución: Profiling y optimización
- Agregar logging automático: Registrar llamadas a funciones
- Validar argumentos: Verificar tipos y valores antes de ejecutar
- Cachear resultados: Almacenar resultados de funciones costosas
- Controlar acceso: Autenticación y autorización en APIs
- Reintentar operaciones: Manejar fallos temporales automáticamente
Ejemplo real: En Flask, @app.route('/ruta') es un decorador que registra una función como endpoint. En Django, @login_required es un decorador que verifica autenticación.
💡 Concepto base
Los decoradores te permiten "envolver" funciones para agregar funcionalidad sin modificar su código original. Es como poner una función dentro de otra que le agrega superpoderes.
Lo genial de Python: La sintaxis @decorador hace que los decoradores sean muy legibles y fáciles de usar.
# Decorador simple
def medir_tiempo(func):
def wrapper(*args, **kwargs):
import time
inicio = time.time()
resultado = func(*args, **kwargs)
fin = time.time()
print(f"{func.__name__} tardó {fin - inicio:.2f} segundos")
return resultado
return wrapper
# Usar el decorador
@medir_tiempo
def preparar_chilaquiles():
time.sleep(1) # Simular trabajo
return "Chilaquiles listos"
preparar_chilaquiles()
preparar_chilaquiles tardó 1.00 segundos
Chilaquiles listos
Los decoradores son como los aderezos y toppings que agregas a los chilaquiles al pastor sin cambiar la receta base. Puedes agregar crema, cebolla, cilantro o aguacate usando decoradores: @agregar_crema, @agregar_cebolla. La receta de los chilaquiles sigue siendo la misma, pero cada decorador le agrega algo extra. De la misma forma, puedes decorar una función con @medir_tiempo o @cachear sin modificar la función original. La función hace su trabajo, el decorador agrega el "topping".
Conceptos Básicos
Decorador Simple
Un decorador es una función que toma otra función y devuelve una función modificada:
def mi_decorador(funcion):
# Esta función recibe una función como parámetro
def wrapper():
# Esta función "envuelve" la función original
print("Antes de la función")
funcion() # Ejecuta la función original
print("Después de la función")
return wrapper # Devuelve la función envuelta
# Aplicar el decorador con @
@mi_decorador
def saludar():
print("¡Hola!")
# Cuando llamas saludar(), en realidad se ejecuta wrapper()
saludar()
# Salida:
# Antes de la función
# ¡Hola!
# Después de la función
¿Qué está pasando aquí?
@mi_decoradores azúcar sintáctico parasaludar = mi_decorador(saludar)mi_decoradorrecibe la funciónsaludaroriginal- Crea una nueva función
wrapperque ejecuta código antes y después - Devuelve
wrapper, que reemplaza asaludar - Cuando llamas
saludar(), en realidad ejecutaswrapper()
En la práctica: Esto te permite agregar funcionalidad (logging, timing, validación) sin tocar el código de saludar().
Decorador con Argumentos
def decorador_con_args(funcion):
def wrapper(*args, **kwargs):
print(f"Argumentos: {args}, {kwargs}")
return funcion(*args, **kwargs)
return wrapper
@decorador_con_args
def sumar(a, b):
return a + b
# Uso
resultado = sumar(5, 3)
Decoradores con Parámetros
Decorador que Acepta Parámetros
def repetir(veces):
def decorador(funcion):
def wrapper(*args, **kwargs):
for _ in range(veces):
resultado = funcion(*args, **kwargs)
return resultado
return wrapper
return decorador
@repetir(veces=3)
def saludar(nombre):
print(f"¡Hola, {nombre}!")
# Uso
saludar("Juan")
Decorador con Estado
def contador_llamadas(funcion):
def wrapper(*args, **kwargs):
wrapper.llamadas += 1
print(f"Llamada #{wrapper.llamadas}")
return funcion(*args, **kwargs)
wrapper.llamadas = 0
return wrapper
@contador_llamadas
def ejemplo():
print("Función ejecutada")
# Uso
ejemplo() # Llamada #1
ejemplo() # Llamada #2
Decoradores de Clase
Decorador de Clase
def decorador_clase(cls):
class Wrapper:
def __init__(self, *args, **kwargs):
self.wrapped = cls(*args, **kwargs)
def __getattr__(self, name):
return getattr(self.wrapped, name)
return Wrapper
@decorador_clase
class MiClase:
def __init__(self, x):
self.x = x
def metodo(self):
return self.x
Decorador de Método
def validar_entero(funcion):
def wrapper(self, valor):
if not isinstance(valor, int):
raise TypeError("El valor debe ser un entero")
return funcion(self, valor)
return wrapper
class Calculadora:
@validar_entero
def sumar(self, x):
return x + 1
Decoradores Útiles
Medir Tiempo
import time
from functools import wraps
def medir_tiempo(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = funcion(*args, **kwargs)
fin = time.time()
print(f"Tiempo de ejecución: {fin - inicio:.2f} segundos")
return resultado
return wrapper
@medir_tiempo
def operacion_lenta():
time.sleep(1)
return "Completado"
Cache
from functools import wraps
def cache(funcion):
cache = {}
@wraps(funcion)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache:
cache[key] = funcion(*args, **kwargs)
return cache[key]
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Validación
def validar_tipos(*tipos_args, **tipos_kwargs):
def decorador(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
# Validar argumentos posicionales
for arg, tipo in zip(args, tipos_args):
if not isinstance(arg, tipo):
raise TypeError(f"Argumento {arg} debe ser de tipo {tipo}")
# Validar argumentos nombrados
for nombre, valor in kwargs.items():
if nombre in tipos_kwargs:
tipo = tipos_kwargs[nombre]
if not isinstance(valor, tipo):
raise TypeError(f"Argumento {nombre} debe ser de tipo {tipo}")
return funcion(*args, **kwargs)
return wrapper
return decorador
@validar_tipos(int, str)
def procesar_datos(numero, texto):
return f"{texto}: {numero}"
Buenas Prácticas
- Preservar Metadatos
from functools import wraps
def mi_decorador(funcion):
@wraps(funcion) # Preserva metadatos
def wrapper(*args, **kwargs):
return funcion(*args, **kwargs)
return wrapper
- Manejo de Errores
def manejar_errores(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
try:
return funcion(*args, **kwargs)
except Exception as e:
print(f"Error en {funcion.__name__}: {e}")
raise
return wrapper
- Documentación
def documentar(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
"""
Documentación del decorador.
"""
return funcion(*args, **kwargs)
return wrapper
Ejemplos Avanzados
Decorador para Logging
import logging
from functools import wraps
def log_llamadas(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
logging.info(f"Llamando a {funcion.__name__} con args={args}, kwargs={kwargs}")
try:
resultado = funcion(*args, **kwargs)
logging.info(f"{funcion.__name__} retornó {resultado}")
return resultado
except Exception as e:
logging.error(f"Error en {funcion.__name__}: {e}")
raise
return wrapper
Decorador para Retry
import time
from functools import wraps
def retry(max_intentos=3, delay=1):
def decorador(funcion):
@wraps(funcion)
def wrapper(*args, **kwargs):
intentos = 0
while intentos < max_intentos:
try:
return funcion(*args, **kwargs)
except Exception as e:
intentos += 1
if intentos == max_intentos:
raise
print(f"Intento {intentos} fallido: {e}")
time.sleep(delay)
return wrapper
return decorador
@retry(max_intentos=3, delay=1)
def operacion_riesgosa():
# Código que puede fallar
pass
Consejos
- Usa
@wrapspara preservar metadatos - Mantén los decoradores simples y reutilizables
- Documenta el comportamiento del decorador
- Considera el impacto en el rendimiento
- Usa decoradores para aspectos transversales
Recursos Adicionales
Documentación Oficial
- Documentación de functools
- PEP 318 - Decorators for Functions and Methods
- Decorator Pattern
- Python Decorators Tutorial
Bibliografía Recomendada
- Fluent Python (Luciano Ramalho) - Capítulo 7: Function Decorators and Closures
- Effective Python (Brett Slatkin) - Items sobre decoradores
- Python Tricks (Dan Bader) - Capítulo completo sobre decoradores
- Python Cookbook, 3rd Ed (Beazley & Jones) - Recetas sobre decoradores
- Design Patterns: Elements of Reusable OOP (Gang of Four) - Patrón Decorator
Wrappers Avanzados
Wrapper con Preservación de Metadatos
from functools import wraps
def medir_tiempo(funcion):
@wraps(funcion) # Preserva metadatos de la función original
def wrapper(*args, **kwargs):
import time
inicio = time.time()
resultado = funcion(*args, **kwargs)
fin = time.time()
print(f"{funcion.__name__} tomó {fin - inicio:.2f} segundos")
return resultado
return wrapper
@medir_tiempo
def operacion_lenta():
import time
time.sleep(1)
return "Completado"
operacion_lenta()
Wrapper con Validación
from functools import wraps
def validar_entero_positivo(funcion):
@wraps(funcion)
def wrapper(numero):
if not isinstance(numero, int):
raise TypeError("El argumento debe ser un entero")
if numero <= 0:
raise ValueError("El número debe ser positivo")
return funcion(numero)
return wrapper
@validar_entero_positivo
def calcular_factorial(n):
"""Calcula el factorial de n"""
if n == 1:
return 1
return n * calcular_factorial(n - 1)
print(calcular_factorial(5)) # 120
Wrappers para Clases
def agregar_metodo_logging(cls):
"""Agrega logging a todos los métodos de una clase"""
for nombre, metodo in vars(cls).items():
if callable(metodo) and not nombre.startswith('_'):
setattr(cls, nombre, log_llamadas(metodo))
return cls
def log_llamadas(funcion):
def wrapper(*args, **kwargs):
print(f"Llamando a {funcion.__name__}")
return funcion(*args, **kwargs)
return wrapper
@agregar_metodo_logging
class Calculadora:
def sumar(self, a, b):
return a + b
def restar(self, a, b):
return a - b
calc = Calculadora()
calc.sumar(5, 3) # Imprime: Llamando a sumar
Conceptos Relacionados
- Funciones - Base para decoradores
- Context Managers - Similar a decoradores
- Clases y Objetos - Decoradores de clase
- Generadores e Iteradores - Para decoradores avanzados