ProgramaciónDesarrollador de Python

¿Qué son los decoradores de Python con argumentos, cómo se implementan, dónde su uso está justificado y qué matices son importantes al escribir tus propios decoradores con parámetros?

Supere entrevistas con el asistente de IA Hintsage

Respuesta

Historia del problema

Los decoradores son una de las herramientas más poderosas de Python, que permiten modificar el comportamiento de funciones o métodos. A veces, no solo necesitas "envolver" una función, sino configurar el decorador utilizando parámetros (argumentos). Estos casos se presentan en registros, verificaciones de tiempo, restricciones de acceso, etc.

Problema

Los decoradores comunes solo aceptan una función que necesita ser envuelta. Cuando se requieren pasar parámetros al decorador, la sintaxis se complica, lo que a menudo lleva a errores, especialmente al tener funciones anidadas y el paso de *args/**kwargs.

Solución

Un decorador con parámetros se implementa mediante una función de orden superior: primero se llama a la función "decoradora" externa con los argumentos, que crea y devuelve el decorador mismo, y el decorador recibe la función y devuelve la envoltura:

def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"¡Hola, {name}!") greet("Python") # Salida: ¡Hola, Python! (3 veces)

Aspectos clave:

  • Un decorador con parámetros siempre se implementa a través de tres niveles de funciones
  • La envoltura debe devolver el resultado, pasando correctamente *args/**kwargs
  • No olvides usar functools.wraps para mantener los metadatos

Preguntas engañosas.

¿Por qué no se puede implementar un decorador con parámetros de la misma manera que un decorador normal?

Si aplicas @decorator, Python pasa la función como argumento al decorador. Si añades paréntesis (@decorator()), Python primero llama a la función, y solo su resultado se interpreta como decorador.

def deco(func): # decorador normal: @deco def deco_with_args(arg): # decorador con argumento: @deco_with_args(arg)

¿En qué se diferencian fundamentalmente los decoradores con argumentos de los decoradores sin argumentos a nivel de llamada?

Los decoradores sin argumentos reciben una función como entrada, mientras que los decoradores con argumentos reciben parámetros, no una función, y devuelven un decorador.

¿Cómo aplicar correctamente functools.wraps y por qué hacerlo?

functools.wraps(func) en la envoltura conserva el nombre, la cadena de documentación y otros metadatos de la función original; de lo contrario, toda esta información se perderá en el wrapper, lo que dificulta la depuración y la introspección.

import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator

Errores típicos y anti-patrones

  • Olvidan la necesidad de tres funciones anidadas, hacen solo dos (o incluso una), el resultado no será un decorador
  • No pasan *args/**kwargs dentro del wrapper
  • Pierden la metainformación de la función debido a la falta de functools.wraps

Ejemplo de la vida real

Caso negativo

Implementaron un decorador sin tener en cuenta los parámetros o con un número incorrecto de funciones anidadas:

def log(level): def wrapper(func): # error - el wrapper debe estar más profundo print(f"Log: {level}") func() # La función no se devuelve como decorador return wrapper @log("INFO") def action(): print("¡Trabajo!")

Ventajas:

  • Se ve simple

Desventajas:

  • El decorador no funciona, la función se llama en el momento de la decoración, no al llamar a action()

Caso positivo

Uso de functools.wraps y funciones anidadas correctas:

import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duración: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()

Ventajas:

  • Estructura correcta, fácil de extender, registros limpios

Desventajas:

  • Más difícil de leer, especialmente para principiantes