Decoradores en Python

Los decoradores son una característica poderosa de Python que te permite modificar o extender el comportamiento de funciones o clases sin cambiar su código directamente.

Concepto Básico

Un decorador es una función que toma otra función como argumento y devuelve una nueva función, usualmente extendiendo o modificando el comportamiento de la función original.

Sintaxis Básica

python
@mi_decorador
def mi_funcion():
    pass

Es equivalente a:

python
def mi_funcion():
    pass
mi_funcion = mi_decorador(mi_funcion)

Crear un Decorador Simple

Ejemplo 1: Decorador para medir tiempo de ejecución

python
import time

def medir_tiempo(funcion):
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = funcion(*args, **kwargs)
        fin = time.time()
        print(f"Tiempo de ejecución: {fin - inicio:.4f} segundos")
        return resultado
    return wrapper

@medir_tiempo
def esperar_segundos(segundos):
    time.sleep(segundos)
    return f"Esperé {segundos} segundos"

# Uso
resultado = esperar_segundos(2)
# Output: Tiempo de ejecución: 2.0023 segundos
print(resultado)  # Output: Esperé 2 segundos

Ejemplo 2: Decorador para verificar login

python
def requiere_login(funcion):
    def wrapper(usuario, *args, **kwargs):
        if usuario.get('autenticado', False):
            return funcion(usuario, *args, **kwargs)
        else:
            return "Error: Usuario no autenticado"
    return wrapper

@requiere_login
def acceder_recurso_secreto(usuario):
    return f"Bienvenido {usuario['nombre']} al recurso secreto"

# Uso
usuario1 = {'nombre': 'Ana', 'autenticado': True}
usuario2 = {'nombre': 'Juan', 'autenticado': False}

print(acceder_recurso_secreto(usuario1))  # Bienvenido Ana al recurso secreto
print(acceder_recurso_secreto(usuario2))  # Error: Usuario no autenticado

Decoradores con Argumentos

Ejemplo 3: Decorador con parámetros

python
def repetir(n_veces):
    def decorador(funcion):
        def wrapper(*args, **kwargs):
            for i in range(n_veces):
                resultado = funcion(*args, **kwargs)
            return resultado
        return wrapper
    return decorador

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

saludar("María")
# Output:
# Hola María
# Hola María
# Hola María

Ejemplo 4: Decorador para validar argumentos

python
def validar_tipo(tipo_esperado):
    def decorador(funcion):
        def wrapper(*args, **kwargs):
            # Validar primer argumento
            if args and not isinstance(args[0], tipo_esperado):
                raise TypeError(f"Se esperaba {tipo_esperado}, se recibió {type(args[0])}")
            return funcion(*args, **kwargs)
        return wrapper
    return decorador

@validar_tipo(int)
def duplicar(numero):
    return numero * 2

print(duplicar(5))    # Output: 10
# print(duplicar("5")) # Esto lanzaría TypeError

Múltiples Decoradores

Puedes aplicar múltiples decoradores a una función:

python
def decorador1(funcion):
    def wrapper():
        print("Decorador 1 - antes")
        funcion()
        print("Decorador 1 - después")
    return wrapper

def decorador2(funcion):
    def wrapper():
        print("Decorador 2 - antes")
        funcion()
        print("Decorador 2 - después")
    return wrapper

@decorador1
@decorador2
def mi_funcion():
    print("Función original")

mi_funcion()
# Output:
# Decorador 1 - antes
# Decorador 2 - antes
# Función original
# Decorador 2 - después
# Decorador 1 - después

Decoradores de Clases

También puedes usar decoradores con clases:

Ejemplo 5: Decorador para clases

python
def agregar_metodo(cls):
    def nuevo_metodo(self):
        return f"Soy un método agregado a {self.__class__.__name__}"
    
    cls.metodo_agregado = nuevo_metodo
    return cls

@agregar_metodo
class MiClase:
    def __init__(self, nombre):
        self.nombre = nombre

objeto = MiClase("Test")
print(objeto.metodo_agregado())  # Soy un método agregado a MiClase

Decoradores Built-in de Python

Python incluye algunos decoradores útiles:

@staticmethod y @classmethod

python
class Calculadora:
    @staticmethod
    def sumar(a, b):
        return a + b
    
    @classmethod
    def restar(cls, a, b):
        return a - b

print(Calculadora.sumar(5, 3))    # Output: 8
print(Calculadora.restar(5, 3))   # Output: 2

@property

python
class Persona:
    def __init__(self, nombre, edad):
        self._nombre = nombre
        self._edad = edad
    
    @property
    def nombre(self):
        return self._nombre
    
    @property
    def edad(self):
        return self._edad
    
    @edad.setter
    def edad(self, valor):
        if valor >= 0:
            self._edad = valor
        else:
            raise ValueError("La edad no puede ser negativa")

persona = Persona("Ana", 25)
print(persona.nombre)  # Ana
print(persona.edad)    # 25
persona.edad = 30
print(persona.edad)    # 30

Decoradores con functools.wraps

Es importante usar functools.wraps para preservar los metadatos de la función original:

python
import functools

def mi_decorador(funcion):
    @functools.wraps(funcion)
    def wrapper(*args, **kwargs):
        print("Antes de la función")
        resultado = funcion(*args, **kwargs)
        print("Después de la función")
        return resultado
    return wrapper

@mi_decorador
def ejemplo():
    """Esta es una función de ejemplo"""
    pass

print(ejemplo.__name__)  # ejemplo (sin wraps sería 'wrapper')
print(ejemplo.__doc__)   # Esta es una función de ejemplo

Casos de Uso Comunes

  1. Logging y monitoreo

  2. Validación de argumentos

  3. Control de acceso y autenticación

  4. Cache y memoización

  5. Retry en caso de errores

  6. Medición de performance

Ejemplo 6: Decorador para cache

python
def cache(funcion):
    cache_dict = {}
    
    @functools.wraps(funcion)
    def wrapper(*args):
        if args in cache_dict:
            print("Resultado desde cache")
            return cache_dict[args]
        resultado = funcion(*args)
        cache_dict[args] = resultado
        print("Resultado calculado")
        return resultado
    return wrapper

@cache
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)

print(factorial(5))  # Calcula y cachea
print(factorial(5))  # Usa cache

Los decoradores son una herramienta extremadamente poderosa en Python que te permite escribir código más limpio, modular y reutilizable. Son ampliamente usados en frameworks web como Flask y Django.

Comentarios

Entradas populares de este blog

¿Qué es un Closure?

Calculadora de edad

Funciones en Python: con y sin paréntesis