35 · Decoradores avanzados: fábricas, argumentos, apilamiento
Ve más allá del @decorador simple — escribe fábricas de decoradores configurables, apila múltiples decoradores correctamente y domina los decoradores de la biblioteca estándar que aparecen en todo codebase de ML.
Una **fábrica de decoradores** es una función que *devuelve* un decorador, añadiendo un nivel extra de anidamiento para poder pasar argumentos: `@retry(times=3)` llama `retry(times=3)` que devuelve un decorador que envuelve la función. Los decoradores apilados (`@a` encima de `@b`) se aplican de abajo hacia arriba: `f = a(b(f))`. `functools.lru_cache` es el decorador más impactante de la biblioteca estándar — convierte cualquier función pura en una memoizada.
Sin esto:
Sin fábricas de decoradores no puedes expresar 'aplica este comportamiento transversal con configuración' — toda lógica de reintentos, límite de tamaño de caché o límite de tasa necesitaría estar codificada fija o pasarse como argumento en cada llamada de función en lugar de declararse una vez en el momento de la definición.
Ya sabes que un decorador simple es una función que toma una función y devuelve una función. Una fábrica de decoradores es un nivel más profundo: es una función que toma argumentos de configuración y devuelve un decorador.
# Decorador simple — sin paréntesis en el sitio de uso
@timer
def f(): ...
# equivalente a: f = timer(f)
# Fábrica de decoradores — paréntesis requeridos en el sitio de uso
@retry(times=3)
def g(): ...
# equivalente a: g = retry(times=3)(g)
# ^^^^^^^^^^^ esta llamada devuelve un decorador
# ^^^ esta llamada envuelve g
La regla: si ves paréntesis en la línea @, es una fábrica, no un decorador simple. La fábrica se llama primero (con los argumentos de configuración), y el resultado es el decorador que envuelve la función.
Los decoradores apilados se aplican de abajo hacia arriba — el decorador más cercano al def se aplica primero. @a encima de @b significa a(b(f)): b envuelve f primero, luego a envuelve el resultado.
Python (in browser)
El anidamiento de tres niveles — `retry` → `decorator` → `wrapper` — es la forma canónica de una fábrica de decoradores. `@functools.wraps(fn)` en el `wrapper` interno es esencial: sin él `flaky_fetch.__name__` sería `'wrapper'` y `help(flaky_fetch)` mostraría el docstring incorrecto.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`lru_cache(maxsize=None)` (o equivalentemente `functools.cache` en Python 3.9+) almacena cada mapping `(args,)` → valor de retorno indefinidamente. `fib(35)` sin caché hace ~29 millones de llamadas recursivas; con caché hace exactamente 36 (una por entrada única). El método `cache_info()` te permite inspeccionar la tasa de hits en tiempo de ejecución — útil para ajustar `maxsize`.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Cuándo usar cada uno: `@property` para lecturas calculadas (y escrituras validadas); `@staticmethod` para funciones utilitarias que pertenecen lógicamente a la clase pero no necesitan `self` ni `cls`; `@classmethod` para constructores alternativos; `@cached_property` para atributos costosos que se calculan una vez y se reutilizan.
Lee los decoradores apilados de abajo hacia arriba para el orden de aplicación, y de arriba hacia abajo para el orden de ejecución. `@bold` encima de `@italic` significa que `bold` es el wrapper más externo — su código corre primero cuando se llama `greet()`, luego llama al wrapper de `italic`, que llama al `greet` original. El modelo mental: cada decorador añade una capa de cebolla, y la llamada pela la cebolla de afuera hacia adentro.
¿Cuál es la diferencia entre escribir @retry y @retry(3) encima de una definición de función?
- Una fábrica de decoradores es una función que devuelve un decorador, habilitando la sintaxis `@retry(times=3)`. La fábrica se llama primero (con la configuración), y su valor de retorno es el decorador aplicado a la función.
- Los decoradores apilados se aplican de abajo hacia arriba: `@a` encima de `@b` significa `f = a(b(f))` — `b` envuelve primero, `a` envuelve el resultado.
- `@functools.lru_cache(maxsize=None)` (o `@functools.cache` en 3.9+) memoiza una función pura — cada tupla de argumentos única se calcula exactamente una vez.
- Siempre aplica `@functools.wraps(fn)` al wrapper más interno para que `__name__`, `__doc__` y `__module__` sobrevivan la decoración.
- Kit de decoradores de la biblioteca estándar: `@property` / `@x.setter`, `@staticmethod`, `@classmethod`, `@functools.cached_property`, `@functools.lru_cache`, `@dataclasses.dataclass`.
`@torch.no_grad()` es una fábrica de decoradores — los paréntesis son requeridos, y devuelve un context manager / decorador que deshabilita el tracking de gradientes para inferencia. `@functools.lru_cache` es ubicuo para cachear inputs tokenizados, transformaciones de features y lookups de vocabulario. `@retry(times=3)` es el patrón estándar para llamadas HTTP inestables en pipelines de recolección de datos y serving de modelos. `@functools.cached_property` es común en subclases de `nn.Module` para embeddings cargados de forma perezosa y propiedades de configuración calculadas.
Si lo quitas: Sin fábricas de decoradores, las preocupaciones transversales configurables (lógica de reintentos, caché con tamaños personalizados, control de gradientes) necesitarían tejerse manualmente en cada cuerpo de función — haciéndolas más difíciles de reutilizar, probar y activar/desactivar. Las fábricas de decoradores son la razón por la que puedes añadir `@retry(times=3)` a una función de obtención de datos y no pensar nunca más en la lógica de reintentos.