20 · Closures y decoradores
Funciones que recuerdan su entorno de origen, y la sintaxis @decorador que envuelve cualquier callable sin tocar su código fuente.
Un **closure** es una función que captura variables de su scope envolvente y las mantiene vivas después de que la función exterior retorna. Un **decorador** es un closure que envuelve otra función — `@dec` es azúcar sintáctica para `f = dec(f)`.
Sin esto:
Sin closures y decoradores, las preocupaciones transversales (temporización, logging, caché, deshabilitar gradientes) deben copiarse en cada función que las necesita. Un decorador aplica la preocupación una sola vez y deja la función original limpia.
Un closure se crea cuando una función anidada referencia variables del scope de su función envolvente (exterior). Python mantiene esas variables vivas en un objeto celda incluso después de que la función exterior retorna. Usa nonlocal para mutar una variable del scope envolvente desde dentro de la función anidada (la misma regla que global, un nivel de scope hacia arriba). Un decorador es una función que toma una función como argumento y devuelve una nueva función (envuelta). La sintaxis @decorador sobre un def es abreviatura de fn = decorador(fn) — Python la aplica automáticamente en el momento de definir la función. Siempre usa functools.wraps(original) dentro de un decorador para preservar el __name__, __doc__ y otros metadatos de la función original.
Python (in browser)
`make_counter` devuelve `counter` — la función interior. Cada llamada a `make_counter` crea una celda `count` fresca, por lo que `epoch_counter` y `batch_counter` son completamente independientes a pesar de compartir la misma definición de `counter`.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`@timer` envuelve `sum_of_squares` — cada llamada ahora imprime su tiempo de ejecución. `*args, **kwargs` en `wrapper` reenvía todos los argumentos sin cambios, por lo que el decorador es transparente para quienes llaman.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Sin `@functools.wraps`, cada función decorada reporta su nombre como `'wrapper'` — rompiendo `help()`, el logging y los tracebacks. Siempre inclúyelo.
¿De qué es abreviatura `@dec` encima de un `def f():`?
El descenso de gradiente se apoya en closures: la función de gradiente *captura* la función de pérdida y los parámetros actuales del modelo, formando un closure que puede evaluarse en cualquier punto del espacio de parámetros. Cada paso del optimizador llama a este closure con los pesos actuales para obtener la dirección del descenso más pronunciado.
- Un closure captura variables de su scope envolvente en *celdas* que sobreviven a la función exterior. Usa `nonlocal` para mutarlas.
- `@decorador` es abreviatura de `f = decorador(f)`. Un decorador es una función que recibe una función y devuelve una función (generalmente envuelta).
- Siempre usa `@functools.wraps(original)` dentro de un decorador para preservar `__name__`, `__doc__` y otros metadatos de la función envuelta.
El decorador `@torch.no_grad()` de PyTorch deshabilita el seguimiento de gradientes para los bucles de inferencia y evaluación — sin él, cada forward pass durante la validación desperdicia memoria construyendo un grafo de cómputo. `@functools.lru_cache` memoiza transformaciones de datos costosas o llamadas de tokenización. Los hooks de logging y profilers envuelven métodos del modelo con decoradores para inyectar temporización sin modificar el código fuente del modelo.
Si lo quitas: Sin decoradores, agregar temporización, logging o deshabilitar gradientes a una función requiere editar su código fuente — violando el principio abierto/cerrado y haciendo el código más difícil de mantener. Un único `@timer` o `@torch.no_grad()` reemplaza docenas de bloques manuales `t0 = time.perf_counter()` / `with torch.no_grad():` dispersos por el codebase.