Python

Boas Práticas de Generators e yield em Python: Lazy Evaluation e Pipelines de Dados para Times Ágeis

11 min de leitura

Boas Práticas de Generators e yield em Python: Lazy Evaluation e Pipelines de Dados para Times Ágeis

Entendendo Generators: O Que Realmente São Um generator em Python é uma função que produz uma sequência de valores, mas diferentemente de uma função comum que retorna todos os valores de uma vez, um generator entrega esses valores sob demanda. Internamente, um generator mantém seu estado entre chamadas sucessivas, permitindo que você processe grandes volumes de dados sem carregar tudo na memória simultânea. A forma mais simples de criar um generator é usar a palavra-chave dentro de uma função. Quando Python encontra , a função não retorna imediatamente — ela pausa sua execução e retorna o valor especificado. Na próxima chamada, a função resume exatamente de onde parou. Isso é fundamentalmente diferente de , que encerra a função e descarta seu estado local. A diferença crucial está no uso de memória. A função comum cria e armazena toda a lista em memória antes de retornar. O generator, por sua vez, calcula cada valor sob demanda e descarta o anterior, ocupando

<h2>Entendendo Generators: O Que Realmente São</h2>

<p>Um generator em Python é uma função que produz uma sequência de valores, mas diferentemente de uma função comum que retorna todos os valores de uma vez, um generator entrega esses valores sob demanda. Internamente, um generator mantém seu estado entre chamadas sucessivas, permitindo que você processe grandes volumes de dados sem carregar tudo na memória simultânea.</p>

<p>A forma mais simples de criar um generator é usar a palavra-chave <code>yield</code> dentro de uma função. Quando Python encontra <code>yield</code>, a função não retorna imediatamente — ela pausa sua execução e retorna o valor especificado. Na próxima chamada, a função resume exatamente de onde parou. Isso é fundamentalmente diferente de <code>return</code>, que encerra a função e descarta seu estado local.</p>

<pre><code class="language-python"># Função comum que retorna uma lista

def numeros_normais():

resultado = []

for i in range(5):

resultado.append(i * 2)

return resultado

Generator que produz valores sob demanda

def numeros_generator():

for i in range(5):

yield i * 2

Usando a função comum

print(numeros_normais()) # [0, 2, 4, 6, 8]

Usando o generator

gen = numeros_generator()

print(next(gen)) # 0

print(next(gen)) # 2

print(next(gen)) # 4</code></pre>

<p>A diferença crucial está no uso de memória. A função comum cria e armazena toda a lista em memória antes de retornar. O generator, por sua vez, calcula cada valor sob demanda e descarta o anterior, ocupando uma fração do espaço de memória.</p>

<h2>Lazy Evaluation: Computação Sob Demanda</h2>

<p>Lazy evaluation (avaliação preguiçosa) é o princípio central que torna generators poderosos. Em vez de processar dados antecipadamente, você computa apenas o que é necessário, quando é necessário. Isso é especialmente valioso ao trabalhar com dados infinitos, fluxos contínuos ou arquivos gigantes.</p>

<p>Considere um cenário real: você precisa processar um arquivo com 10 gigabytes de linhas. Se você usasse uma abordagem tradicional, teria que carregar o arquivo inteiro na memória antes de processar a primeira linha. Com lazy evaluation, você lê e processa linha por linha, mantendo apenas a atual na memória.</p>

<pre><code class="language-python"># Abordagem tradicional (problema de memória)

def ler_arquivo_inteiro(caminho):

linhas = []

with open(caminho, &#039;r&#039;) as f:

for linha in f:

linhas.append(linha.strip())

return linhas

Abordagem com lazy evaluation

def ler_arquivo_lazy(caminho):

with open(caminho, &#039;r&#039;) as f:

for linha in f:

yield linha.strip()

Processando dados

for linha in ler_arquivo_lazy(&#039;dados.txt&#039;):

processar(linha) # Apenas uma linha na memória por vez</code></pre>

<h3>Expressões Geradoras</h3>

<p>Python oferece uma sintaxe compacta para criar generators sem usar <code>def</code> e <code>yield</code>. Expressões geradoras funcionam como list comprehensions, mas usam parênteses em vez de colchetes:</p>

<pre><code class="language-python"># List comprehension (cria lista inteira na memória)

quadrados_lista = [x**2 for x in range(1000000)]

Expressão geradora (computa sob demanda)

quadrados_gen = (x**2 for x in range(1000000))

print(type(quadrados_lista)) # &lt;class &#039;list&#039;&gt;

print(type(quadrados_gen)) # &lt;class &#039;generator&#039;&gt;

Consumindo o generator

for quad in quadrados_gen:

if quad &gt; 50:

print(quad)

break</code></pre>

<p>Expressões geradoras são ideais quando você precisa processar dados sequencialmente e não necessita acessá-los aleatoriamente (sem usar índices).</p>

<h2>Pipelines de Dados com Generators</h2>

<p>Um pipeline de dados é uma série de transformações aplicadas sequencialmente a um fluxo de dados. Generators são perfeitos para construir pipelines porque cada estágio processa dados sob demanda, criando um fluxo eficiente que nunca mantém os dados completos na memória.</p>

<p>A arquitetura é simples: cada função geradora consome dados de um gerador anterior e produz dados transformados para o próximo. Essa abordagem separa responsabilidades, facilita testes e otimiza o consumo de memória.</p>

<pre><code class="language-python"># Estágio 1: Leitura de dados

def ler_numeros(inicio, fim):

for i in range(inicio, fim):

print(f&quot; Lendo {i}&quot;)

yield i

Estágio 2: Filtrar números pares

def filtrar_pares(sequencia):

for numero in sequencia:

if numero % 2 == 0:

print(f&quot; Filtrando {numero}&quot;)

yield numero

Estágio 3: Multiplicar por 2

def multiplicar(sequencia, fator):

for numero in sequencia:

resultado = numero * fator

print(f&quot; Multiplicando {numero} por {fator} = {resultado}&quot;)

yield resultado

Construir o pipeline

pipeline = multiplicar(

filtrar_pares(

ler_numeros(1, 11)

),

fator=10

)

Consumir o pipeline

print(&quot;\nResultados:&quot;)

for resultado in pipeline:

print(f&quot;Resultado final: {resultado}&quot;)</code></pre>

<p><strong>Saída esperada:</strong></p>

<pre><code>Lendo 1

Lendo 2

Filtrando 2

Multiplicando 2 por 10 = 20

Resultado final: 20

Lendo 3

Lendo 4

Filtrando 4

Multiplicando 4 por 10 = 40

Resultado final: 40

[...]</code></pre>

<h3>Pipelines Práticos com CSV</h3>

<p>Aqui está um exemplo realista de processamento de dados CSV sem carregar tudo na memória:</p>

<pre><code class="language-python">import csv

from datetime import datetime

Estágio 1: Ler CSV linha por linha

def ler_csv(caminho):

with open(caminho, &#039;r&#039;, encoding=&#039;utf-8&#039;) as f:

leitor = csv.DictReader(f)

for linha in leitor:

yield linha

Estágio 2: Filtrar registros válidos

def filtrar_validos(linhas):

for linha in linhas:

try:

float(linha[&#039;valor&#039;])

yield linha

except (ValueError, KeyError):

continue

Estágio 3: Enriquecer com data de processamento

def adicionar_timestamp(linhas):

for linha in linhas:

linha[&#039;data_processamento&#039;] = datetime.now().isoformat()

yield linha

Estágio 4: Agrupar por categoria

def agrupar_por_categoria(linhas):

grupos = {}

for linha in linhas:

categoria = linha.get(&#039;categoria&#039;, &#039;sem_categoria&#039;)

if categoria not in grupos:

grupos[categoria] = []

grupos[categoria].append(linha)

for categoria, items in grupos.items():

yield {&#039;categoria&#039;: categoria, &#039;itens&#039;: items}

Executar pipeline

pipeline = agrupar_por_categoria(

adicionar_timestamp(

filtrar_validos(

ler_csv(&#039;vendas.csv&#039;)

)

)

)

Processar resultado

for grupo in pipeline:

print(f&quot;Categoria: {grupo[&#039;categoria&#039;]}, Quantidade: {len(grupo[&#039;itens&#039;])}&quot;)</code></pre>

<h3>Composição com itertools</h3>

<p>A biblioteca padrão <code>itertools</code> oferece generators prontos que facilitam composição de pipelines:</p>

<pre><code class="language-python">import itertools

Dados de exemplo

dados = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Pipeline: pegar 5 primeiros, filtrar pares, multiplicar por 2

resultado = map(

lambda x: x * 2,

filter(

lambda x: x % 2 == 0,

itertools.islice(dados, 5)

)

)

print(list(resultado)) # [4, 8]

Usando itertools.chain para combinar múltiplas sequências

seq1 = [1, 2, 3]

seq2 = [4, 5, 6]

combinado = itertools.chain(seq1, seq2)

for valor in combinado:

print(valor) # 1, 2, 3, 4, 5, 6

itertools.cycle para repetir sequências infinitamente

ciclo = itertools.cycle([&#039;A&#039;, &#039;B&#039;, &#039;C&#039;])

print([next(ciclo) for _ in range(5)]) # [&#039;A&#039;, &#039;B&#039;, &#039;C&#039;, &#039;A&#039;, &#039;B&#039;]</code></pre>

<h2>Erros Comuns e Armadilhas</h2>

<h3>Consumindo Generators Múltiplas Vezes</h3>

<p>Um erro frequente é tentar iterar sobre o mesmo generator duas vezes. Após ser consumido, um generator é esgotado e não pode ser reutilizado.</p>

<pre><code class="language-python">gen = (x**2 for x in range(5))

Primeira iteração funciona

print(list(gen)) # [0, 1, 4, 9, 16]

Segunda iteração retorna vazio

print(list(gen)) # []</code></pre>

<p>Se você precisa reutilizar os dados, converta para lista (se a memória permitir) ou crie um novo generator:</p>

<pre><code class="language-python"># Solução 1: Converter para lista (cuidado com memória)

dados = list(gen)

print(list(dados))

print(list(dados))

Solução 2: Criar função que retorna novo generator

def criar_gen():

return (x**2 for x in range(5))

print(list(criar_gen()))

print(list(criar_gen()))</code></pre>

<h3>StopIteration e next()</h3>

<p>Ao usar <code>next()</code> diretamente, você recebe <code>StopIteration</code> quando o generator se esgota. Use a forma segura com valor padrão:</p>

<pre><code class="language-python">gen = (x for x in range(3))

print(next(gen)) # 0

print(next(gen)) # 1

print(next(gen)) # 2

print(next(gen, None)) # None (ao invés de StopIteration)</code></pre>

<h3>Debugging de Generators</h3>

<p>Generators podem ser difíceis de debugar porque não computam valores até serem consumidos. Uma técnica útil é usar <code>itertools.tee()</code> para criar cópias independentes:</p>

<pre><code class="language-python">import itertools

gen = (x**2 for x in range(5))

Criar duas cópias independentes

gen1, gen2 = itertools.tee(gen, 2)

Debugar uma cópia sem afetar a outra

print(&quot;Debug:&quot;, list(gen1))

print(&quot;Consumir:&quot;, list(gen2))</code></pre>

<h2>Conclusão</h2>

<p>Os três pontos fundamentais que você aprendeu neste artigo são:</p>

<ol>

<li><strong>Generators são máquinas de estado</strong> que mantêm contexto entre chamadas, viabilizando a implementação de lazy evaluation sem necessidade de armazenar dados completos em memória. Isso transforma a forma como você trabalha com dados grandes ou contínuos.</li>

</ol>

<ol>

<li><strong>Pipelines de dados com generators</strong> oferecem composição elegante e eficiente de transformações, onde cada estágio processa sob demanda. Essa abordagem separa responsabilidades, facilita testes unitários e reduz drasticamente o consumo de memória em comparação com abordagens que materializam dados intermediários.</li>

</ol>

<ol>

<li><strong>Expressões geradoras e a biblioteca itertools</strong> fornecem ferramentas prontas que eliminam a necessidade de escrever generators manualmente em muitos casos, tornando o código mais conciso e legível mantendo todos os benefícios de lazy evaluation.</li>

</ol>

<p>Domine esses conceitos e você terá uma ferramenta poderosa para lidar com processamento de dados em escala, desde leitura de arquivos gigantes até construção de pipelines ETL complexos.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.python.org/3/howto/functional.html#generators" target="_blank" rel="noopener noreferrer">Python Official Documentation - Generators</a></li>

<li><a href="https://realpython.com/generators-and-yield-in-python/" target="_blank" rel="noopener noreferrer">Real Python - Generators and yield</a></li>

<li><a href="https://www.dabeaz.com/generators/" target="_blank" rel="noopener noreferrer">David Beazley - Generator Tricks for Systems Programmers</a></li>

<li><a href="https://www.oreilly.com/library/view/fluent-python-2nd/9781492126249/" target="_blank" rel="noopener noreferrer">Fluent Python - Luciano Ramalho (Capítulo sobre Iterables e Generators)</a></li>

<li><a href="https://docs.python.org/3/library/itertools.html" target="_blank" rel="noopener noreferrer">Python itertools Documentation</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Guia Completo de Automação de Tarefas em Python: subprocess, shutil e Scripting Real
Guia Completo de Automação de Tarefas em Python: subprocess, shutil e Scripting Real

Entendendo Automação de Tarefas em Python Automação de tarefas é um dos pilar...

Testes de Integração em Python: Banco Real com pytest e Docker na Prática
Testes de Integração em Python: Banco Real com pytest e Docker na Prática

Entendendo Testes de Integração: O que são e por que importam Testes de integ...

Guia Completo de Módulos e Pacotes em Python: import, __init__ e Organização de Projetos
Guia Completo de Módulos e Pacotes em Python: import, __init__ e Organização de Projetos

Entendendo o Sistema de Módulos e Pacotes em Python Um módulo em Python é sim...