Saltar al contenido principal
Avanzado⏱ 1.5 horas
Prerequisitos: Funciones · Clases y Objetos
Módulo 07: Conceptos AvanzadosLección 2 de 4
50% completado

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()
Resultado esperado:
preparar_chilaquiles tardó 1.00 segundos
Chilaquiles listos
🌮 Analogía culinaria

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".

Para principiantes

Antes de continuar: Asegúrate de entender bien Funciones y Clases. Los decoradores son conceptos avanzados pero muy poderosos. La ventaja clave: escribes la funcionalidad una vez, la aplicas a muchas funciones con solo agregar @decorador arriba.

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í?

  1. @mi_decorador es azúcar sintáctico para saludar = mi_decorador(saludar)
  2. mi_decorador recibe la función saludar original
  3. Crea una nueva función wrapper que ejecuta código antes y después
  4. Devuelve wrapper, que reemplaza a saludar
  5. Cuando llamas saludar(), en realidad ejecutas wrapper()

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​

  1. Preservar Metadatos
from functools import wraps

def mi_decorador(funcion):
@wraps(funcion) # Preserva metadatos
def wrapper(*args, **kwargs):
return funcion(*args, **kwargs)
return wrapper
  1. 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
  1. 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​

  1. Usa @wraps para preservar metadatos
  2. Mantén los decoradores simples y reutilizables
  3. Documenta el comportamiento del decorador
  4. Considera el impacto en el rendimiento
  5. Usa decoradores para aspectos transversales

Recursos Adicionales​

Documentación Oficial​

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​

Siguiente paso​