Decoradores: azúcar sintáctico desazucarado en Python

Un decorador es, sin más, una función que recibe otra función como argumento y devuelve algo callable. Nada especial, nada mágico. Lo que hace especial a @mi_decorador es que Python lo transforma automáticamente en una asignación: cuando escribes @decorador justo antes de una definición de función, el intérprete ejecuta f = decorador(f) inmediatamente después de crear f. El nombre f en el namespace queda apuntando al resultado de esa llamada, no a la función original.

Esto es posible porque en Python las funciones son objetos de primera clase: se pueden pasar como argumentos, devolver como valores, asignar a variables, guardar en listas. No hay distinción entre una función y cualquier otro objeto. La sintaxis @ no introduce ningún concepto nuevo; simplemente te ahorra escribir f = decorador(f) debajo de cada definición.

El momento adecuado para usar un decorador es cuando tienes comportamiento transversal que quieres aplicar a varias funciones sin modificar su lógica interna: logging, validación de argumentos, caché, control de acceso, medición de tiempo. Si te encuentras copiando el mismo bloque de código antes y después de la lógica real en varias funciones, ahí hay un decorador esperando.

El error conceptual más caro es olvidar que el nombre original desaparece del namespace en el momento de la decoración. Si el decorador retorna None por accidente (o retorna wrapper pero sin llamar a func), el daño ya está hecho: cada llamada posterior a f falla de formas que pueden ser difíciles de rastrear.

Ejemplo principal

import functools
import time


def timer(func):
    """Mide cuánto tarda func en ejecutarse."""
    @functools.wraps(func)  # preserva __name__, __doc__, etc. de func
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)          # ejecuta la función original
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} tardó {elapsed:.6f}s")
        return result                            # propaga el valor de retorno
    return wrapper                               # reemplaza func con wrapper


# Esto:
@timer
def compute_sum(n: int) -> int:
    return sum(range(n))


# Es exactamente equivalente a esto (no uses ambas formas a la vez):
# def compute_sum(n: int) -> int:
#     return sum(range(n))
# compute_sum = timer(compute_sum)


if __name__ == "__main__":
    total = compute_sum(10_000_000)
    print(f"Resultado: {total}")

    # La función decorada conserva su identidad gracias a functools.wraps
    print(compute_sum.__name__)   # → compute_sum (no "wrapper")

Lo que está pasando de verdad

Cuando Python procesa @timer sobre compute_sum, ejecuta literalmente compute_sum = timer(compute_sum). En ese instante, timer recibe el objeto función original y crea wrapper como un closure: wrapper captura func de su scope envolvente, así que tiene acceso permanente a la función original incluso después de que timer haya retornado.

*args, **kwargs en la firma de wrapper es la decisión correcta aquí: hace que el wrapper sea agnóstico a la interfaz de func. Si usaras parámetros fijos, el decorador quedaría acoplado a una firma concreta y dejaría de ser reutilizable.

El return result al final de wrapper es crítico. Si lo omites, compute_sum(...) devuelve None silenciosamente aunque la función original produzca un valor. Es el error más silencioso y frecuente en decoradores caseros.

functools.wraps(func) merece atención especial. Sin él, compute_sum.__name__ sería "wrapper", compute_sum.__doc__ estaría vacío, y herramientas como help(), Sphinx, o cualquier introspección basada en __name__ verían el wrapper, no la función real. functools.wraps copia los atributos del dunder relevantes desde func hacia wrapper, y también actualiza __wrapped__, que permite a herramientas (y a ti mismo) acceder a la función original con compute_sum.__wrapped__.

El patrón def decorador(func) → def wrapper → return wrapper no es una convención arbitraria: es la estructura mínima que cumple el contrato. timer recibe func, construye algo nuevo que lo envuelve, y lo devuelve. Python hace el resto.

Errores que debes conocer

Error: el decorador no retorna el wrapper, devuelve None implícitamente, y el nombre original queda inutilizable.

# ❌ Wrong
def timer(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    # se olvidó el return — timer retorna None

@timer
def greet():
    return "hola"

greet()  # TypeError: 'NoneType' object is not callable
# ✅ Right
def timer(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper  # el namespace recibe wrapper, no None

timer debe retornar explícitamente el callable que reemplazará a func; sin ese return, la asignación f = timer(f) vincula f a None.

Error: el wrapper no propaga el valor de retorno de func, descartando silenciosamente resultados.

# ❌ Wrong
def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)   # resultado ignorado
    return wrapper

@log
def add(a, b):
    return a + b

result = add(2, 3)
print(result)   # None — el 5 se perdió
# ✅ Right
def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)   # propaga el resultado
    return wrapper

Agregar return delante de la llamada a func garantiza que el wrapper sea transparente para el caller respecto al valor de retorno.

116

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio