Um script responsável por resumir o diário de pagamentos funcionou sem incidentes durante um ano, antes de falhar num ficheiro demasiado grande. Um tutorial dedicado aos iteradores e aos geradores de Python mostra como construir cadeias de processamento que leem os dados um registo de cada vez, sem os carregar todos para a memória.
- Um script de pagamentos falhou porque carregava todos os registos numa lista.
- Os iteradores e geradores leem os dados um registo de cada vez, gastando pouca memória.
- Os geradores não são mais rápidos, mas limitam a memória, e operações como a ordenação global continuam a exigir todos os registos.
- Num teste de uma única execução, a versão em lista atingiu 198 MB, contra 0,4 MB na versão em fluxo.
No caso descrito, o script abria o ficheiro, construía uma lista de registos analisados, filtrava os erros e depois exibia uma tabela. Após uma alteração na rotação dos diários, uma semana de tráfego ficou num único ficheiro de 9 GB. O processamento noturno interrompeu-se com um MemoryError antes de produzir qualquer resumo. Segundo o autor, a lógica de análise não estava em causa: o problema vinha da lista, que obriga a manter todos os registos ao mesmo tempo.
O funcionamento baseia-se num contrato simples. Um iterável, como uma lista, uma cadeia de caracteres ou um ficheiro aberto, pode fornecer um iterador, ou seja, um cursor que avança elemento a elemento e nunca retrocede. O ciclo for chama estas etapas automaticamente, e o fim da sequência é sinalizado pela exceção StopIteration, que não é um erro. Um iterador esgotado não se reinicializa: uma função que devolve « nada » numa segunda leitura não está necessariamente vazia, simplesmente já foi consumida.
O tutorial recomenda começar pelo objeto ficheiro, que já é um iterador. Ler um ficheiro com f.readlines() cria uma lista com todas as linhas, enquanto que for line in f lê apenas um bloco tamponado de cada vez. As funções geradoras, que usam a palavra-chave yield, permitem depois dividir a análise em etapas nomeadas. A instrução yield from encadeia vários ficheiros de rotação como um único fluxo, e as expressões geradoras, escritas entre parênteses, servem para pequenas transformações intermédias. O autor alerta para uma armadilha: a parte mais à esquerda de uma expressão geradora é avaliada imediatamente.
Medir antes de confiar
O tutorial também insiste nos pontos onde a lista regressa discretamente. As chamadas list(), sorted(), um dicionário que agrupa os registos por identificador de pedido, ou a construção de um DataFrame reintroduzem a totalidade do fluxo na memória. Os geradores também não são mais rápidos do que as listas: o seu interesse é uma memória limitada e etapas reorganizáveis. Para os processamentos em lote, o Python 3.12 propõe itertools.batched(), e é dada uma receita equivalente em islice para as versões anteriores.
Para verificar o comportamento real, o autor recomenda o módulo tracemalloc, que mede a memória alocada pelo Python, em valor corrente e em pico. Num diário sintético de 200 000 linhas, com cerca de 17 MB, a versão « lista » atinge um pico de 198 MB num computador portátil, contra 0,4 MB para a versão em fluxo. O autor precisa que se trata dos resultados de uma única execução, e não de um benchmark. Recorda também que o tracemalloc não vê as alocações das extensões em C nem a cache do sistema.
Os limites do fluxo
Alguns processamentos não podem ser conduzidos em fluxo. Uma ordenação global, uma desduplicação exata sobre todo o ficheiro ou um percentil calculado sobre todos os valores de latência exigem o conhecimento de todos os registos. Nesse caso, o tutorial menciona um algoritmo aproximado baseado numa amostra, ou uma ferramenta externa que ordena em disco.
O tutorial precisa ainda que esta abordagem permanece em Python puro. Pandas, Spark, Dask ou os fluxos assíncronos respondem a problemas semelhantes, mas não são necessários para um ficheiro de diário, uma exportação ou um despejo de dados que cabe no disco sem caber na memória RAM.
