Ir al contenido principal

Decoradores: creación y aplicación

Introducción

Los decoradores son una característica poderosa de Python que permite modificar el comportamiento de funciones o clases sin cambiar su código. Esta funcionalidad avanzada forma parte del paradigma de programación funcional y representa una forma elegante de aplicar el principio de composición de funciones. Los decoradores nos permiten "envolver" una función con otra función para extender su comportamiento, agregar funcionalidades o modificar sus entradas y salidas, todo ello manteniendo el código limpio y siguiendo el principio DRY (Don't Repeat Yourself).

En este artículo, exploraremos qué son los decoradores, cómo crearlos y las formas más comunes de aplicarlos en nuestros programas Python.

Entendiendo los decoradores

En esencia, un decorador es una función que toma otra función como argumento, agrega alguna funcionalidad y devuelve otra función, todo sin modificar el código de la función original.

Para entender cómo funcionan los decoradores, recordemos que en Python las funciones son objetos de primera clase, lo que significa que pueden ser:

  • Asignadas a variables
  • Pasadas como argumentos a otras funciones
  • Devueltas como resultado de otras funciones

Veamos un ejemplo sencillo:

def saludo():
    return "¡Hola mundo!"

# Asignar una función a una variable
mi_funcion = saludo
print(mi_funcion())  # Imprime: ¡Hola mundo!

Funciones que devuelven funciones

Los decoradores se basan en la capacidad de Python para crear funciones que devuelven otras funciones:

def crear_funcion_saludo(nombre):
    def saludar():
        return f"¡Hola, {nombre}!"
    return saludar

saludo_maria = crear_funcion_saludo("María")
print(saludo_maria())  # Imprime: ¡Hola, María!

Creación de un decorador básico

Ahora veamos cómo crear un decorador básico:

def mi_decorador(funcion):
    def funcion_envoltorio():
        print("Código que se ejecuta antes de la función")
        funcion()  # Llamamos a la función original
        print("Código que se ejecuta después de la función")
    return funcion_envoltorio

# Aplicación manual del decorador
def saludar():
    print("¡Hola!")

saludar_decorada = mi_decorador(saludar)
saludar_decorada()  # Ejecuta la función decorada

La salida sería:

Código que se ejecuta antes de la función
¡Hola!
Código que se ejecuta después de la función

Sintaxis de decoración con @

Python proporciona una sintaxis especial para aplicar decoradores utilizando el símbolo @:

def mi_decorador(funcion):
    def funcion_envoltorio():
        print("Código que se ejecuta antes de la función")
        funcion()
        print("Código que se ejecuta después de la función")
    return funcion_envoltorio

# Aplicación del decorador usando la sintaxis @
@mi_decorador
def saludar():
    print("¡Hola!")

saludar()  # Ahora saludar ya está decorada

Esta sintaxis @mi_decorador es equivalente a saludar = mi_decorador(saludar).

Decoradores con argumentos de función

Los decoradores anteriores funcionan bien para funciones sin argumentos, pero ¿qué pasa con las funciones que tienen parámetros? Podemos adaptar nuestros decoradores así:

def mi_decorador(funcion):
    def funcion_envoltorio(*args, **kwargs):
        print("Código que se ejecuta antes de la función")
        resultado = funcion(*args, **kwargs)  # Llamamos a la función original con sus argumentos
        print("Código que se ejecuta después de la función")
        return resultado
    return funcion_envoltorio

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

resultado = suma(5, 3)  # Imprime los mensajes del decorador y devuelve 8
print(f"El resultado es: {resultado}")

Decoradores con parámetros

También podemos crear decoradores que acepten sus propios parámetros:

def repetir(n):
    def decorador_repetir(funcion):
        def funcion_envoltorio(*args, **kwargs):
            resultado = None
            for _ in range(n):
                resultado = funcion(*args, **kwargs)
            return resultado
        return funcion_envoltorio
    return decorador_repetir

@repetir(3)
def saludar(nombre):
    print(f"¡Hola {nombre}!")
    return f"Saludo a {nombre} completado"

resultado = saludar("Ana")  # Imprime "¡Hola Ana!" tres veces
print(resultado)  # Imprime: Saludo a Ana completado

En este caso, la estructura es un poco más compleja. Tenemos tres niveles de funciones:

  1. repetir(n): La función exterior que acepta el argumento del decorador
  2. decorador_repetir(funcion): El decorador real que recibe la función
  3. funcion_envoltorio(*args, **kwargs): La función envoltorio que se ejecutará

Casos de uso comunes para decoradores

Los decoradores tienen muchas aplicaciones prácticas:

  1. Registrar acciones (logging):
def registrar(funcion):
    def funcion_envoltorio(*args, **kwargs):
        print(f"Llamando a la función: {funcion.__name__}")
        resultado = funcion(*args, **kwargs)
        print(f"La función {funcion.__name__} retornó: {resultado}")
        return resultado
    return funcion_envoltorio

@registrar
def multiplicar(a, b):
    return a * b

multiplicar(4, 5)
  1. Medir tiempo de ejecución:
import time

def medir_tiempo(funcion):
    def funcion_envoltorio(*args, **kwargs):
        inicio = time.time()
        resultado = funcion(*args, **kwargs)
        fin = time.time()
        print(f"La función {funcion.__name__} tardó {fin - inicio:.5f} segundos en ejecutarse")
        return resultado
    return funcion_envoltorio

@medir_tiempo
def proceso_lento():
    time.sleep(1)  # Simula un proceso que tarda 1 segundo
    return "Proceso completado"

proceso_lento()
  1. Control de acceso:
def requiere_autenticacion(funcion):
    def funcion_envoltorio(*args, **kwargs):
        # Simulamos una comprobación de autenticación
        usuario_autenticado = True  # En un caso real, esto vendría de una sesión
        
        if usuario_autenticado:
            return funcion(*args, **kwargs)
        else:
            return "Acceso denegado. Por favor, inicie sesión."
    
    return funcion_envoltorio

@requiere_autenticacion
def pagina_admin():
    return "Bienvenido al panel de administración"

print(pagina_admin())
  1. Validación de datos:
def validar_enteros_positivos(funcion):
    def funcion_envoltorio(*args, **kwargs):
        # Validamos que todos los argumentos posicionales sean enteros positivos
        if not all(isinstance(arg, int) and arg > 0 for arg in args):
            raise ValueError("Todos los argumentos deben ser enteros positivos")
        return funcion(*args, **kwargs)
    return funcion_envoltorio

@validar_enteros_positivos
def calcular_area_rectangulo(ancho, alto):
    return ancho * alto

try:
    print(calcular_area_rectangulo(5, 10))  # Funciona correctamente
    print(calcular_area_rectangulo(5, -2))  # Lanza un error
except ValueError as e:
    print(e)

Encadenando decoradores

Podemos aplicar múltiples decoradores a una función, y se aplicarán de abajo hacia arriba:

def decorador_a(funcion):
    def funcion_envoltorio(*args, **kwargs):
        print("Decorador A - Antes")
        resultado = funcion(*args, **kwargs)
        print("Decorador A - Después")
        return resultado
    return funcion_envoltorio

def decorador_b(funcion):
    def funcion_envoltorio(*args, **kwargs):
        print("Decorador B - Antes")
        resultado = funcion(*args, **kwargs)
        print("Decorador B - Después")
        return resultado
    return funcion_envoltorio

@decorador_a
@decorador_b
def saludar():
    print("¡Hola mundo!")

saludar()

La salida será:

Decorador A - Antes
Decorador B - Antes
¡Hola mundo!
Decorador B - Después
Decorador A - Después

Preservando metadatos de la función original

Un problema con los decoradores es que pierden los metadatos de la función original como el nombre, la documentación, etc. Para solucionar esto, podemos usar la función wraps del módulo functools:

from functools import wraps

def mi_decorador(funcion):
    @wraps(funcion)  # Preserva los metadatos de la función original
    def funcion_envoltorio(*args, **kwargs):
        """Esta es la documentación del envoltorio"""
        print("Antes de llamar a la función")
        resultado = funcion(*args, **kwargs)
        print("Después de llamar a la función")
        return resultado
    return funcion_envoltorio

@mi_decorador
def saludar(nombre):
    """Esta función saluda a la persona dada"""
    return f"¡Hola, {nombre}!"

# Sin @wraps, esto mostraría el nombre y la doc de funcion_envoltorio
print(saludar.__name__)  # Imprime: saludar
print(saludar.__doc__)   # Imprime: Esta función saluda a la persona dada

Resumen

Los decoradores son una poderosa herramienta de Python que nos permite extender el comportamiento de funciones o clases sin modificar su código. Hemos aprendido a crear decoradores simples, decoradores con argumentos de función, decoradores que aceptan parámetros propios y hemos visto casos de uso comunes como registro, medición de tiempo, control de acceso y validación de datos.

El uso adecuado de decoradores puede mejorar significativamente la modularidad y la reutilización de nuestro código. A medida que sigas avanzando en tu aprendizaje de Python, encontrarás que muchos frameworks y bibliotecas (como Flask, Django o incluso las propias bibliotecas estándar de Python) hacen un uso extensivo de decoradores para proporcionar funcionalidades de manera elegante y mantenible.