Los decoradores son una característica poderosa y elegante en Python que permite modificar o extender el comportamiento de funciones o métodos de una manera muy legible y reutilizable. Imagina que tienes una función y quieres añadirle funcionalidades extra, como registrar cuándo se ejecuta, verificar los argumentos, o incluso medir su tiempo de ejecución. En lugar de modificar la función directamente, puedes usar un decorador para envolverla con esta nueva funcionalidad.

En este artículo, exploraremos a fondo los decoradores en Python. Desde su definición y sintaxis básica hasta ejemplos prácticos y casos de uso avanzados, aprenderás cómo aprovechar al máximo esta herramienta para escribir código más limpio, modular y eficiente.

Introducción a los Decoradores

En esencia, un decorador es una función que toma otra función como argumento, añade alguna funcionalidad a esta función y luego la retorna. Python ofrece una sintaxis especial, utilizando el símbolo @, que facilita la aplicación de decoradores de manera concisa y legible.

Para entender mejor, veamos un ejemplo sencillo. Imaginemos que queremos crear un decorador que simplemente imprima un mensaje antes y después de la ejecución de una función:


def mi_decorador(func):
    def funcion_envolvente(*args, **kwargs):
        print("Antes de ejecutar la función.")
        resultado = func(*args, **kwargs)
        print("Después de ejecutar la función.")
        return resultado
    return funcion_envolvente

Aquí, mi_decorador es una función que toma otra función (func) como argumento. Dentro de mi_decorador, definimos una función interna llamada funcion_envolvente. Esta función envolvente es la que realmente añade la funcionalidad extra: imprime mensajes antes y después de ejecutar la función original (func). Finalmente, mi_decorador retorna funcion_envolvente.

Para aplicar este decorador a una función, usamos la sintaxis @:


@mi_decorador
def saludar(nombre):
    print(f"Hola, {nombre}!")

Ahora, cuando llamamos a saludar("Juan"), el resultado será:


Antes de ejecutar la función.
Hola, Juan!
Después de ejecutar la función.

Como puedes ver, el decorador mi_decorador ha modificado el comportamiento de la función saludar sin necesidad de modificar su código interno.

Cómo Crear un Decorador

Crear un decorador implica definir una función que acepte otra función como argumento y retorne una nueva función (generalmente una función envolvente) que contenga la lógica adicional. Hay algunos puntos clave a tener en cuenta al crear decoradores:

  1. La función envolvente (wrapper): Debe aceptar argumentos arbitrarios (*args y **kwargs) para que pueda ser utilizada con cualquier función, independientemente de sus parámetros.
  2. Retorno del valor original: La función envolvente debe retornar el valor que retorna la función original. Esto asegura que el decorador no altere el comportamiento fundamental de la función original, sino que simplemente lo extienda.
  3. Preservar la metadata: Para mantener la información de la función original (nombre, documentación, etc.), se puede usar functools.wraps.

Veamos un ejemplo más completo que incluye estos aspectos:


import functools

def rastrear_llamadas(func):
    @functools.wraps(func)
    def funcion_envolvente(*args, **kwargs):
        print(f"Llamando a la función: {func.__name__} con argumentos: {args} y kwargs: {kwargs}")
        resultado = func(*args, **kwargs)
        print(f"La función {func.__name__} retornó: {resultado}")
        return resultado
    return funcion_envolvente

@rastrear_llamadas
def sumar(a, b):
    """Esta función suma dos números."""
    return a + b

print(sumar(5, 3))
print(sumar.__doc__)
print(sumar.__name__)

En este ejemplo, el decorador rastrear_llamadas registra las llamadas a la función decorada, mostrando los argumentos y el valor de retorno. functools.wraps(func) copia la metadata de func a funcion_envolvente, por lo que sumar.__doc__ y sumar.__name__ funcionan como se espera.

Ejemplos Prácticos

Los decoradores pueden usarse para una variedad de propósitos. Aquí te muestro algunos ejemplos prácticos:

  • Medición del tiempo de ejecución:

import time

def medir_tiempo(func):
    @functools.wraps(func)
    def funcion_envolvente(*args, **kwargs):
        inicio = time.time()
        resultado = func(*args, **kwargs)
        fin = time.time()
        print(f"La función {func.__name__} tardó {fin - inicio:.4f} segundos en ejecutarse.")
        return resultado
    return funcion_envolvente

@medir_tiempo
def tarea_larga():
    time.sleep(2)

tarea_larga()
  • Validación de argumentos:

def validar_argumentos(tipo_esperado):
    def decorador_real(func):
        @functools.wraps(func)
        def funcion_envolvente(*args, **kwargs):
            for arg in args:
                if not isinstance(arg, tipo_esperado):
                    raise ValueError(f"Argumento inválido: {arg}. Se esperaba un tipo {tipo_esperado}")
            return func(*args, **kwargs)
        return funcion_envolvente
    return decorador_real

@validar_argumentos(int)
def multiplicar(a, b):
    return a * b

print(multiplicar(5, 3))
# print(multiplicar(5, "hola")) # Esto lanzará un ValueError
  • Logging:

import logging

logging.basicConfig(level=logging.INFO)

def loggear_funcion(func):
    @functools.wraps(func)
    def funcion_envolvente(*args, **kwargs):
        logging.info(f"Ejecutando {func.__name__} con args: {args}, kwargs: {kwargs}")
        resultado = func(*args, **kwargs)
        logging.info(f"{func.__name__} retornó: {resultado}")
        return resultado
    return funcion_envolvente

@loggear_funcion
def dividir(a, b):
    return a / b

dividir(10, 2)

Casos de Uso en el Mundo Real

Los decoradores se utilizan ampliamente en diversos contextos del mundo real. Algunos ejemplos incluyen:

  • Frameworks web (Flask, Django): Para definir rutas, gestionar sesiones, autenticación y autorización.

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "¡Hola, mundo!"

if __name__ == "__main__":
    app.run(debug=True)
  • Testing: Para configurar pruebas, ejecutar código antes/después de cada prueba, y verificar condiciones.

import unittest

def ejecutar_antes_despues(func):
    def envolver():
      print("before")
      func()
      print("after")
    return envolver

class MyTest(unittest.TestCase):
    @ejecutar_antes_despues
    def test_example(self):
        self.assertEqual(1, 1)

if __name__ == '__main__':
    unittest.main()
  • Caché: Para almacenar en caché los resultados de funciones costosas y evitar recalcularlos innecesariamente.

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Estos son solo algunos ejemplos, pero los decoradores pueden aplicarse en cualquier situación donde quieras añadir comportamiento adicional a una función de forma modular y reutilizable. La clave está en identificar patrones comunes y abstraerlos en decoradores.

 

Los decoradores son una herramienta esencial en el arsenal de cualquier desarrollador Python. Permiten escribir código más limpio, modular y reutilizable al separar la lógica principal de una función de la lógica accesoria. Al dominar los decoradores, podrás crear aplicaciones más robustas, mantenibles y fáciles de entender. Recuerda practicar con los ejemplos y adaptarlos a tus propias necesidades para internalizar completamente este concepto.

Ads Blocker Image Powered by Code Help Pro

Por favor, permite que se muestren anuncios en nuestro sitio web

Querido lector,

Esperamos que estés disfrutando de nuestro contenido. Entendemos la importancia de la experiencia sin interrupciones, pero también queremos asegurarnos de que podamos seguir brindándote contenido de alta calidad de forma gratuita. Desactivar tu bloqueador de anuncios en nuestro sitio nos ayuda enormemente a lograrlo.

¡Gracias por tu comprensión y apoyo!