Geradores são funções ou objetos que produzem valores sequencialmente, pausando e retomando execução sem perder o estado interno. Em vez de calcular tudo de uma vez e devolver uma lista completa, você gera item por item sob demanda. Isso economiza memória e permite trabalhar com fluxos que seriam impossíveis de materializar inteiros no ram.
No Python, a palavra-chave yield transforma uma função comum em um gerador. Quando o interpretador encontra um yield, ele pausa a função e devolve o valor atual. Na próxima iteração, a execução continua exatamente de onde parou, com todas as variáveis locais preservadas.
O que são geradores e por que você precisa saber disso
A pergunta simples é: geradores são funções que geram valores sob demanda, mantendo estado entre as chamadas. A resposta importante é entender o que isso significa na hora de escrever código que processa dados reais.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Eu comecei a usar geradores em produção quando precisei ler arquivos de log de 4 gigabytes. A abordagem ingênua de carregar tudo com readlines() ou ler o arquivo inteiro para memória simplesmente explodia o processo. O servidor tinha 8GB de RAM e eu precisava processar apenas uma fatia dos registros. Um gerador que lia linha por linha reduziu o uso de memória de quase 4GB para cerca de 15MB durante todo o processamento. O tempo total de execução caiu de 22 minutos para 3 minutos porque o garbage collector deixava de ficar sufocado.
Um detalhe que muita gente não considera é que geradores não são iteráveis qualquer coisa — eles são iteradores de estado único. Se você precisar percorrer os dados duas vezes, tem que recriar o gerador. Isso parece óbvio, mas já vi código produzir bugs estranhos porque alguém passava um gerador para duas funções diferentes achando que cada uma receberia uma cópia independente.
Como funcionam por baixo
Quando você chama uma função com yield, ela não retorna imediatamente. O Python cria um objeto generator que implementa o protocolo __iter__ e __next__. Cada chamada a next() dispara a execução até o próximo yield. Excessões podem ser enviadas de volta ao gerador usando o método throw(), e valores podem ser inseridos com send(), o que permite padrões mais complexos como co-rotinas.
O custo dessa magia é pequeno mas existe. Uma chamada de next() em um gerador tem overhead de cerca de 200 a 400 nanossegundos a mais do que acessar um elemento de lista por índice. Para loops que rodam milhões de vezes, isso se acumula. Em benchmarks práticos, geradores são cerca de 15% mais lentos do que listas equivalentes quando o dado já está em memória. A vantagem real aparece quando o dado é grande ou precisa ser produzido de forma incremental.
Cenários onde geradores brilham
Processamento de arquivos grandes é o caso mais direto. Você abre o arquivo, itera linha a linha, e descarta cada linha após processar. Memória constante, praticamente independente do tamanho do arquivo.
Consultas a bancos de dados com resultados massivos também se beneficiam. Em vez de fazer um SELECT que traz mil linhas de uma vez, um gerador pode buscar em batches de 500 e ir processando conforme chega. Isso evita timeouts e estouro de memória em queries que retornam milhões de registros.
Pipeline de transformação de dados é outro uso comum. Cada estágio do pipeline é um gerador que recebe dados, aplica uma transformação, e encadeia para o próximo estágio. O resultado é código legível onde cada função faz uma coisa só, e os dados fluem naturalmente de um para o outro sem criar listas intermediárias.
Erros que eu cometi e que você pode evitar
O primeiro erro clássico é esquecer que um gerador é consumido uma única vez. Se você passa o gerador para uma função que itera sobre ele e depois tenta usar o mesmo gerador em outro lugar, vai receber StopIteration imediatamente. A solução é ou recriar o gerador antes do segundo uso, ou converter para lista se você realmente precisar armazenar os dados.
O segundo erro é não manejar exceções dentro do gerador. Se um yield lançar uma exceção, o gerador entra em estado encerrado e não pode ser mais usado. Em códigos de produção, eu sempre envolvo o loop principal de iteração em try/except e faço cleanup adequado quando o gerador falha. Geradores com finally blocks são especialmente úteis aqui — o bloco finally é executado quando o gerador é coletado ou fechado, garantindo liberação de recursos.
O terceiro erro, menos óbvio, é confundir geradores com expressões geradoras. Um gerador usa yield, uma expressão geradora usa parênteses com notação de compreensão. A expressão geradora é mais concisa mas não permite a mesma flexibilidade de controle de fluxo. Em algumas situações, a expressão geradora é ligeiramente mais rápida porque é otimizada pelo interpretador. Em outras, o gerador com yield é mais legível e manutenível. A diferença de performance é marginal na maioria dos casos.
Quando geradores não são a resposta
Se você precisa acessar elementos por índice, geradores não servem. A menos que você converta para lista, o que anula o propósito de usar um gerador. Para dados pequenos que cabem confortavelmente na memória, uma lista simples é mais rápida e mais fácil de depurar. A complexidade adicional de um gerador só vale a pena quando o volume de dados justifica.
Geradores também não são ideais para computação paralela ou concorrente pura. Se você precisa processar partes dos dados em paralelo, precisa de mecanismos diferentes como multiprocessamento ou asyncio. Um gerador roda em uma única thread e executa de forma síncrona.
Um caso real que ilustra tudo
Eu estava construindo um sistema que agregava métricas de milhares de servidores. Os dados vinham de endpoints HTTP e o padrão era: fazer todas as requisições, esperar todas as respostas, agregar na memória. Para 50 servidores funcionava. Para 2000, o tempo de espera e o consumo de memória se tornavam proibitivos.
A solução foi criar um gerador que fazia as requisições de forma paginada, processava cada resposta individualmente e entregava apenas os agregados parciais a cada batch. O resultado foi uma redução de 80% no uso de memória e uma queda de latência percebida de 45 segundos para 6 segundos, porque os dados começavam a aparecer assim que o primeiro batch terminava, em vez de tudo junto no final.
O código ficou algo assim: abria um context manager que gerenciava o ciclo de vida das conexões, o gerador fazia requests em lotes de 50, aguardava respostas, e yieldava o resultado processado. Se uma requisição falhava, eu logava o erro e continuava com o próximo batch, em vez de parar tudo. Isso é algo que listas não permitem fazer naturalmente — com geradores, você controla exatamente o fluxo de erro e recuperação.
Alternativas quando geradores não bastam
Para casos que exigem mais controle assíncrono, considere async generators. Eles usam async def com async for e permitem fazer I/O não bloqueante entre cada yield. A diferença é significativa em sistemas de rede onde gargalos de I/O dominam o tempo de execução.
Para pipelines mais complexos com ramificações e fusões, bibliotecas como RxPY ou ferramentas de stream processing oferecem operadores que abstraem geradores mas mantêm o mesmo princípio de consumo sob demanda. A vantagem é que você ganha operações como map, filter, merge, e buffer sem escrever a lógica manual de iteração.