Un script encargado de resumir el registro de pagos funcionó sin incidentes durante un año, antes de fallar con un archivo demasiado grande. Un tutorial dedicado a los iteradores y a los generadores de Python muestra cómo construir cadenas de procesamiento que leen los datos un registro a la vez, sin cargarlos todos en memoria.
En el caso descrito, el script abría el archivo, construía una lista de registros analizados, filtraba los errores y luego mostraba una tabla. Tras un cambio en la rotación de los registros, una semana de tráfico acabó en un solo archivo de 9 GB. El procesamiento nocturno se interrumpió con un MemoryError antes de haber producido el menor resumen. Según el autor, la lógica de análisis no era la culpable: el problema venía de la lista, que obliga a conservar todos los registros al mismo tiempo.
El funcionamiento se basa en un contrato simple. Un iterable, como una lista, una cadena de caracteres o un archivo abierto, puede proporcionar un iterador, es decir, un cursor que avanza elemento por elemento y nunca retrocede. El bucle for llama a estos pasos automáticamente, y el fin de la secuencia se señala con la excepción StopIteration, que no es un error. Un iterador agotado no se reinicia: una función que devuelve « nada » en una segunda lectura no está necesariamente vacía, simplemente ya ha sido consumida.
El tutorial recomienda empezar por el objeto archivo, que ya es un iterador. Leer un archivo con f.readlines() crea una lista de todas las líneas, mientras que for line in f solo lee un bloque almacenado en búfer a la vez. Las funciones generadoras, que utilizan la palabra clave yield, permiten luego dividir el análisis en etapas nombradas. La instrucción yield from encadena varios archivos de rotación como un solo flujo, y las expresiones generadoras, escritas entre paréntesis, sirven para pequeñas transformaciones intermedias. El autor señala una trampa: la parte más a la izquierda de una expresión generadora se evalúa inmediatamente.
Medir antes de confiar
El tutorial también insiste en los puntos donde la lista vuelve discretamente. Las llamadas list(), sorted(), un diccionario que agrupa los registros por identificador de solicitud, o la construcción de un DataFrame reintroducen la totalidad del flujo en memoria. Los generadores tampoco son más rápidos que las listas: su interés es una memoria acotada y etapas reorganizables. Para los procesamientos por lotes, Python 3.12 propone itertools.batched(), y se da una receta equivalente en islice para las versiones anteriores.
Para verificar el comportamiento real, el autor recomienda el módulo tracemalloc, que mide la memoria asignada por Python, en valor corriente y en pico. Sobre un registro sintético de 200 000 líneas, de unos 17 MB, la versión « lista » alcanza un pico de 198 MB en un ordenador portátil, frente a 0,4 MB para la versión en flujo. El autor precisa que se trata de los resultados de una sola ejecución, y no de un benchmark. También recuerda que tracemalloc no ve las asignaciones de las extensiones en C ni la caché del sistema.
Los límites del flujo
Algunos procesamientos no pueden llevarse a cabo en flujo. Un ordenamiento global, una deduplicación exacta sobre la totalidad del archivo o un percentil calculado sobre todos los valores de latencia exigen conocer todos los registros. En este caso, el tutorial menciona un algoritmo aproximado basado en una muestra, o una herramienta externa que ordena en disco.
El tutorial precisa finalmente que este enfoque se mantiene en Python puro. Pandas, Spark, Dask o los flujos asíncronos responden a problemas similares, pero no son necesarios para un archivo de registro, una exportación o un volcado de datos que cabe en el disco sin caber en la memoria RAM.
