Cierres, Protocolos y Evaluación Perezosa
Iteradores, generadores, decoradores y gestores de contexto parecen cuatro características diferentes, pero comparten dos ideas subyacentes: protocolos (objetos que implementan un pequeño conjunto acordado de métodos dunder) y cierres (funciones que transportan estado desde un ámbito envolvente entre llamadas).
for delega al protocolo de iterador, with delega al protocolo de gestor de contexto y @decorator es solo un cierre que envuelve una función; ninguna de estas son casos especiales del lenguaje, son todos objetos ordinarios que implementan los ganchos correctos.
Esta página es el modelo subyacente a Iteradores y el Protocolo de Iterador, Generadores y yield, Gestores de Contexto y Escribir Decoradores; una vez que se entiende, itertools y functools se leen como una caja de herramientas construida sobre estas mismas dos ideas en lugar de un conjunto de utilidades no relacionadas.
Resumen
- La sintaxis
for,withy@decoratorde Python son azúcar sintáctico sobre protocolos - objetos que implementan métodos dunder específicos - y los cierres son cómo esos objetos (o funciones simples) transportan estado entre llamadas. - Por Qué Importa: Una vez que los protocolos y los cierres se ven como el mecanismo compartido, los generadores, decoradores y gestores de contexto dejan de ser temas separados para memorizar y se convierten en tres aplicaciones de una idea.
- Conceptos Clave: protocolo de iterador, evaluación perezosa, cierre / variable libre, protocolo de gestor de contexto, función de orden superior.
- Cuándo Usar: Construir un iterable personalizado, escribir un decorador que necesite recordar estado entre llamadas, o decidir entre un generador y una lista al procesar datos grandes o infinitos.
- Limitaciones / Compensaciones: Los objetos perezosos basados en protocolos (generadores, iteradores) intercambian acceso aleatorio y reutilización por eficiencia de memoria; la mayoría son de un solo paso y no se pueden indexar ni reiniciar.
- Temas Relacionados: el modelo de objetos y el dispatch dunder, ámbito de función (LEGB), iteración asíncrona, memoización.
Fundamentos
Un iterador es cualquier objeto con un método __next__ que devuelve el siguiente valor o lanza StopIteration cuando se agota; un iterable es cualquier objeto con un método __iter__ que devuelve dicho iterador.
for item in obj: es completamente azúcar sintáctico sobre esto: Python llama a iter(obj) una vez para obtener un iterador, luego llama a next() sobre él repetidamente hasta que StopIteration detiene el bucle; no hay otra magia involucrada.
Una función generadora (cualquier función que contenga yield) es la forma más fácil de obtener un iterador sin escribir una clase: llamarla no ejecuta el cuerpo de la función en absoluto, devuelve inmediatamente un objeto generador, y el cuerpo solo se ejecuta hasta el próximo yield cada vez que se llama a next() sobre él.
Esa ejecución diferida es evaluación perezosa: los valores se producen uno a la vez, bajo demanda, en lugar de todos a la vez por adelantado; es por eso que un generador puede representar una secuencia infinita sin quedarse sin memoria.
Un cierre es una función anidada que hace referencia a una variable del ámbito de una función externa; Python mantiene esa variable viva en un objeto celda compartido mientras exista el cierre, que es cómo un decorador o una función similar a un generador puede recordar estado entre llamadas separadas sin una clase.
Mecánicas e Interacciones
La mecánica que más confunde a la gente es que los cierres capturan una referencia a la celda de una variable, no una instantánea de su valor en el momento de la definición; es por eso que un bucle que crea varios cierres sobre una variable del bucle a menudo sorprende a la gente al hacer que todos vean el valor final del bucle.
def make_multiplier(factor):
def multiply(x):
return x * factor # 'factor' se lee de la celda envolvente, en vivo
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5)) # 10 15 - cada cierre mantiene su propia celda 'factor'with obj: es azúcar sintáctico sobre el protocolo de gestor de contexto: Python llama a obj.__enter__() antes del bloque y garantiza que obj.__exit__(exc_type, exc, tb) se ejecute después de él, incluso si el bloque lanzó una excepción; es exactamente por eso que with es la forma idiomática de garantizar la limpieza (cerrar un archivo, liberar un bloqueo) en lugar de un try/finally manual.
@contextlib.contextmanager construye un gestor de contexto a partir de una función generadora en lugar de una clase: el código antes del único yield se convierte en el comportamiento de __enter__, el código después se convierte en el comportamiento de __exit__, y cualquier excepción del interior del bloque with se lanza en ese punto yield dentro del generador, permitiéndole manejarla con un try/except ordinario alrededor del yield.
Un decorador no es más que una función de orden superior: @log_calls encima de una definición de función es exactamente equivalente a escribir my_func = log_calls(my_func) inmediatamente después de definirla, y la función envolvente que devuelve es un cierre que recuerda la función original para poder llamarla más tarde, típicamente después o alrededor de algún comportamiento adicional.
yield from delega la iteración a un subgenerador o a cualquier iterable, reenviando sus valores (y, con menos frecuencia, valores enviados y excepciones) como si el generador externo los hubiera producido él mismo; este es el mecanismo que Yield From y Subgeneradores cubre en profundidad, y existe específicamente para que los generadores puedan componerse sin el código repetitivo manual de for ... yield.
Consideraciones Avanzadas y Aplicaciones
itertools y functools no introducen mecanismos nuevos: son funciones componibles construidas completamente sobre el protocolo de iterador y los cierres ya descritos aquí: itertools.chain devuelve un iterador que recorre perezosamente varios iterables en secuencia, y functools.lru_cache envuelve una función en un cierre que recuerda argumentos y resultados anteriores.
La mayor compensación arquitectónica en este espacio es la evaluación perezosa versus la ansiosa: una comprensión de lista calcula cada elemento inmediatamente y los mantiene todos en memoria, mientras que una expresión generadora o una tubería itertools calcula cada elemento solo cuando se solicita, a costa de ser de un solo paso y no indexable.
itertools.batched (añadido en Python 3.12, con un argumento de palabra clave strict añadido en 3.13) es un ejemplo reciente de esta misma filosofía de composición perezosa: convierte cualquier iterable en un iterador de tuplas de tamaño fijo sin materializar nunca la entrada completa en memoria.
El código asíncrono extiende la misma idea de protocolo a un modelo de ejecución diferente: async for usa __aiter__/__anext__ en lugar de __iter__/__next__, y async with usa __aenter__/__aexit__; el patrón del protocolo es idéntico, solo que los métodos son corrutinas que pueden await dentro de ellos.
| Enfoque | Fortaleza | Debilidad | Mejor ajuste |
|---|---|---|---|
| Comprensión de lista | Simple, indexable, reutilizable varias veces | Materializa todo en memoria inmediatamente | Datos pequeños a medianos que iterarás más de una vez |
| Expresión / función generadora | Memoria constante independientemente de la longitud; soporta secuencias infinitas | Solo de un solo paso; sin indexación ni len() | Datos grandes o ilimitados, pipelines de streaming/ETL |
| Iterador basado en clase personalizada | Control total sobre el estado, soporta métodos adicionales más allá de la iteración | Más código repetitivo que un generador para el mismo comportamiento | Iterables reutilizables que necesitan reiniciarse o exponer API adicionales |
Composición itertools | Perezoso, eficiente en memoria, evita la gestión manual de bucles | Se lee de forma menos lineal para desarrolladores no familiarizados con el módulo | Combinatoria, encadenamiento, segmentación de iteradores sin un pase completo |
Los decoradores se componen de la misma manera a escala: apilar varios decoradores (@a luego @b encima de una función) los aplica de abajo hacia arriba, por lo que func = a(b(func)), y perder el rastro de ese orden - o olvidar functools.wraps para preservar el __name__/__doc__ de la función original - son las dos fuentes más comunes de errores confusos de decoradores en bases de código reales.
Conceptos Erróneos Comunes
- "Un generador es una lista calculada perezosamente." Está más cerca de un iterador de un solo uso: no puedes indexarlo, comprobar su longitud ni reiniciarlo una vez agotado, a diferencia de una lista que resulta estar calculada perezosamente.
- "
fortiene soporte especial integrado para listas, diccionarios y archivos."forsolo llama aiter()y luego anext()repetidamente; cada iterable, integrado o personalizado, pasa por el mismo protocolo idéntico. - "Los cierres copian el valor de la variable envolvente cuando se define la función interna." Capturan una referencia viva a la celda de la variable, razón por la cual los cierres creados en un bucle a menudo terminan viendo el valor final de esa variable, a menos que se capture explícitamente por iteración.
- "Un decorador cambia el código fuente real de la función." Reemplaza el nombre por un objeto de función nuevo (generalmente envolvente); la función original todavía existe, solo que referenciada desde dentro del cierre del envoltorio.
- "
withsolo ejecuta algo de código antes y después de un bloque, como dos llamadas a función." Garantiza específicamente que__exit__se ejecute incluso cuando el bloque lanza una excepción, y__exit__puede inspeccionar o incluso suprimir esa excepción; un par simple de llamadas "antes y después" no puede hacer ninguna de las dos cosas.
Preguntas Frecuentes
¿Qué es el protocolo de iterador, concretamente?
Dos métodos dunder: __iter__, que devuelve un iterador, y __next__ en ese iterador, que devuelve el siguiente valor o lanza StopIteration cuando no queda nada; los bucles for se construyen completamente llamando a estos dos métodos.
¿Llama a una función generadora a su código inmediatamente?
No, llamarla devuelve un objeto generador sin ejecutar ninguna parte del cuerpo de la función; el cuerpo solo se ejecuta hasta el próximo yield cada vez que se llama a next() sobre ese objeto.
¿Qué significa "evaluación perezosa" en este contexto?
Los valores se producen uno a la vez, solo cuando se solicitan, en lugar de calcularse y almacenarse todos por adelantado; esto es lo que permite que un generador represente incluso una secuencia infinita sin agotar la memoria.
¿Qué captura exactamente un cierre: el valor o la variable?
Captura una referencia a la celda de la variable envolvente, no una instantánea de su valor en ese momento, razón por la cual el valor actual de la variable es lo que un cierre ve cada vez que se ejecuta, no lo que sea que fuera cuando se creó el cierre.
¿Por qué los cierres hechos dentro de un bucle a veces se comportan todos igual, inesperadamente?
Porque todos comparten la celda de la misma variable envolvente en lugar de que cada uno reciba su propia copia, por lo que para cuando alguno de ellos se ejecuta realmente, todos leen el valor que esa variable terminó teniendo después de que el bucle finalizó.
¿Qué garantiza realmente `with obj:`?
Que obj.__exit__() se ejecute después de que el bloque se complete, incluso si el bloque lanzó una excepción; obj.__enter__() se ejecuta primero y su valor de retorno es lo que as name enlaza, si se usa.
¿Es `@contextlib.contextmanager` un atajo o un mecanismo completamente diferente?
Es un atajo sobre el mismo protocolo: el código antes del único yield del generador se convierte en el comportamiento de __enter__, y el código después se convierte en el comportamiento de __exit__, con las excepciones del bloque with apareciendo en ese punto yield.
¿Qué es un decorador, mecánicamente, en una oración?
@decorator encima de una definición de función es exactamente equivalente a escribir func = decorator(func) justo después de definirla; nada más misterioso que una llamada a una función de orden superior.
¿Por qué importa `functools.wraps` al escribir un decorador?
Sin él, la función envolvente reemplaza el __name__, __doc__ y otros metadatos del original con sus propios valores genéricos, lo que rompe la introspección, la salida de depuración y las herramientas de documentación que esperan ver la identidad de la función original.
¿Son `itertools` y `functools` un sistema separado de iteradores y cierres?
No; las funciones itertools devuelven iteradores perezosos construidos sobre el mismo protocolo de iterador, y las herramientas functools como lru_cache o partial son cierres que envuelven llamables, por lo que ambos módulos son composiciones de los dos mecanismos que esta página describe.
¿Cuándo debo usar un generador en lugar de una comprensión de lista?
Cuando los datos son grandes, ilimitados o solo necesitan un solo pase; un generador solo mantiene su estado actual en memoria, mientras que una comprensión de lista materializa cada elemento inmediatamente, lo que cuesta más memoria pero permite la reutilización y la indexación.
¿Cómo se relaciona `async for` con el protocolo de iterador regular?
Es el mismo patrón de protocolo aplicado a corrutinas: __aiter__/__anext__ reemplazan __iter__/__next__, y ambos métodos pueden await internamente, permitiendo que la iteración se pause en I/O real en lugar de solo en computación síncrona.
Relacionados
- Iteradores y el Protocolo de Iterador -
__iter__/__next__y iterables personalizados en profundidad - Generadores y yield - estado del generador y pipelines perezosos de cerca
- Gestores de Contexto - el protocolo
__enter__/__exit__ycontextlib - Escribir Decoradores - cierres que envuelven llamables, manejo de argumentos y apilamiento
- Cierres y Captura de Ámbito - la mecánica de captura de celdas en detalle completo
- itertools - composición perezosa construida sobre el protocolo de iterador
Versiones de Stack: Esta página fue escrita para Python 3.14 (estable) y Python 3.13 (mantenimiento), incluyendo
itertools.batched(añadido en 3.12,strict=añadido en 3.13).