Geradores e Iteradores em Python

Entenda como funcionam geradores e iteradores em Python. Tutorial completo com yield, generator expressions, itertools e casos de uso práticos.

8 min de leitura Equipe python.dev.br

Quando você percorre uma lista com um for em Python, está usando o protocolo de iteração sem perceber. Por trás dos panos, Python chama métodos especiais para obter um elemento de cada vez. Entender como iteradores e geradores funcionam é essencial para escrever código eficiente, especialmente quando lidamos com grandes volumes de dados.

O Que São Iteradores

Um iterador é qualquer objeto que implementa o protocolo de iteração: os métodos __iter__() e __next__(). O método __iter__ retorna o próprio objeto iterador, e __next__ retorna o próximo elemento ou levanta StopIteration quando não há mais elementos.

# Listas são iteráveis, mas não são iteradores
numeros = [1, 2, 3]

# iter() cria um iterador a partir de um iterável
iterador = iter(numeros)

print(next(iterador))  # 1
print(next(iterador))  # 2
print(next(iterador))  # 3
# next(iterador)       # StopIteration!

Quando você escreve for item in lista, o Python internamente chama iter() na lista para obter um iterador e depois chama next() repetidamente até receber StopIteration.

Criando um Iterador Customizado

Você pode criar seus próprios iteradores implementando __iter__ e __next__ em uma classe:

class Contagem:
    """Iterador que conta de inicio até fim."""

    def __init__(self, inicio: int, fim: int):
        self.atual = inicio
        self.fim = fim

    def __iter__(self):
        return self

    def __next__(self) -> int:
        if self.atual > self.fim:
            raise StopIteration
        valor = self.atual
        self.atual += 1
        return valor

# Usando o iterador customizado
for num in Contagem(1, 5):
    print(num, end=" ")  # 1 2 3 4 5

Essa abordagem funciona, mas exige bastante código. Para a maioria dos casos, geradores oferecem uma alternativa muito mais concisa.

O Que São Geradores

Geradores são funções que usam a palavra-chave yield em vez de return. Quando chamadas, elas retornam um objeto gerador que implementa automaticamente o protocolo de iteração — sem que você precise escrever __iter__ e __next__ manualmente.

def contagem(inicio: int, fim: int):
    """Gerador que conta de inicio até fim."""
    atual = inicio
    while atual <= fim:
        yield atual
        atual += 1

# Usando o gerador — mesmo resultado, menos código
for num in contagem(1, 5):
    print(num, end=" ")  # 1 2 3 4 5

A diferença fundamental é que o yield pausa a execução da função e retorna o valor. Na próxima chamada a next(), a execução continua exatamente de onde parou, preservando todo o estado local (variáveis, posição no loop, etc.).

yield vs return: Diferenças Fundamentais

A diferença entre yield e return vai além da sintaxe:

# Com return — executa tudo de uma vez, retorna lista completa
def quadrados_lista(n: int) -> list[int]:
    resultado = []
    for i in range(n):
        resultado.append(i ** 2)
    return resultado

# Com yield — produz um valor por vez, sob demanda
def quadrados_gerador(n: int):
    for i in range(n):
        yield i ** 2

# A lista ocupa memória proporcional a n
lista = quadrados_lista(1_000_000)  # ~8MB na memória

# O gerador ocupa memória constante, independente de n
gerador = quadrados_gerador(1_000_000)  # ~120 bytes na memória

Com return, a função calcula todos os valores, armazena em uma lista e retorna tudo de uma vez. Com yield, cada valor é calculado apenas quando solicitado. Essa é a essência da lazy evaluation (avaliação preguiçosa).

Generator Expressions vs List Comprehensions

Assim como existem list comprehensions, existem generator expressions — a versão lazy das comprehensions:

# List comprehension — cria lista inteira na memória
soma_lista = sum([x ** 2 for x in range(1_000_000)])

# Generator expression — calcula sob demanda
soma_gerador = sum(x ** 2 for x in range(1_000_000))

# Ambos dão o mesmo resultado, mas o gerador usa muito menos memória

A sintaxe é quase idêntica: troque os colchetes [] por parênteses (). Quando a generator expression é o único argumento de uma função, você pode omitir os parênteses extras, como no exemplo com sum() acima.

Use generator expressions quando você precisa iterar sobre os valores apenas uma vez. Use list comprehensions quando precisa acessar os elementos múltiplas vezes ou por índice.

Lazy Evaluation e Eficiência de Memória

A principal vantagem dos geradores é a eficiência de memória. Em vez de carregar todos os dados na memória de uma vez, o gerador produz um valor por vez:

import sys

# Comparando uso de memória
lista = [i for i in range(1_000_000)]
gerador = (i for i in range(1_000_000))

print(f"Lista: {sys.getsizeof(lista):,} bytes")     # ~8,448,728 bytes
print(f"Gerador: {sys.getsizeof(gerador):,} bytes")  # ~200 bytes

Isso é especialmente importante quando se trabalha com manipulação de arquivos grandes ou processamento de dados. Um gerador permite processar arquivos de gigabytes sem estourar a memória.

yield from para Delegação de Geradores

O yield from delega a iteração para outro iterável ou gerador, simplificando geradores compostos:

def numeros_pares(n: int):
    for i in range(0, n, 2):
        yield i

def numeros_impares(n: int):
    for i in range(1, n, 2):
        yield i

def todos_os_numeros(n: int):
    yield from numeros_pares(n)
    yield from numeros_impares(n)

# Produz: 0, 2, 4, 6, 8, 1, 3, 5, 7, 9
for num in todos_os_numeros(10):
    print(num, end=" ")

Sem yield from, você precisaria de um loop for explícito para cada sub-gerador. O yield from também propaga exceções e valores de retorno corretamente, o que é útil em geradores mais complexos.

send() e close(): Geradores como Coroutines

Geradores não são apenas produtores de dados — eles também podem receber dados via send():

def acumulador():
    """Gerador que acumula valores recebidos via send()."""
    total = 0
    while True:
        valor = yield total
        if valor is None:
            break
        total += valor

gen = acumulador()
next(gen)           # Inicializa o gerador, retorna 0

print(gen.send(10))  # 10 (total acumulado)
print(gen.send(20))  # 30
print(gen.send(5))   # 35

gen.close()  # Encerra o gerador

O método send() envia um valor para dentro do gerador, que é recebido como resultado da expressão yield. O close() levanta GeneratorExit dentro do gerador, permitindo que ele finalize recursos.

Essa capacidade de enviar e receber dados é a base das coroutines em Python e influenciou diretamente o design do async/await.

Casos de Uso Práticos

Ler Arquivos Grandes Linha a Linha

def ler_linhas(caminho: str):
    """Lê um arquivo grande sem carregar tudo na memória."""
    with open(caminho, "r", encoding="utf-8") as f:
        for linha in f:
            linha = linha.strip()
            if linha:  # ignora linhas vazias
                yield linha

# Processa um arquivo de 10GB usando memória constante
for linha in ler_linhas("logs_servidor.txt"):
    if "ERROR" in linha:
        print(linha)

Pipeline de Processamento de Dados

def ler_csv(caminho: str):
    with open(caminho) as f:
        next(f)  # pula o cabeçalho
        for linha in f:
            yield linha.strip().split(",")

def filtrar_ativos(registros):
    for registro in registros:
        if registro[3] == "ativo":
            yield registro

def extrair_nomes(registros):
    for registro in registros:
        yield registro[1]

# Pipeline: lê -> filtra -> extrai — sem carregar tudo na memória
pipeline = extrair_nomes(filtrar_ativos(ler_csv("clientes.csv")))
for nome in pipeline:
    print(nome)

Séries Infinitas

def fibonacci():
    """Gerador infinito da sequência de Fibonacci."""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Pega apenas os 10 primeiros números de Fibonacci
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

itertools: Ferramentas para Iteração Avançada

O módulo itertools da biblioteca padrão oferece geradores poderosos para manipulação de iteráveis:

from itertools import chain, islice, count, cycle, combinations, groupby

# chain — concatena múltiplos iteráveis
letras = chain("abc", "def", "ghi")
print(list(letras))  # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']

# islice — fatia iteradores (como slice, mas para iteráveis)
naturais = count(1)  # 1, 2, 3, 4, ...
print(list(islice(naturais, 5)))  # [1, 2, 3, 4, 5]

# cycle — repete um iterável infinitamente
cores = cycle(["vermelho", "verde", "azul"])
print([next(cores) for _ in range(7)])
# ['vermelho', 'verde', 'azul', 'vermelho', 'verde', 'azul', 'vermelho']

# combinations — todas as combinações possíveis
print(list(combinations([1, 2, 3, 4], 2)))
# [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]

O itertools é essencial para quem trabalha com estruturas de dados e precisa de operações eficientes sobre sequências.

Boas Práticas: Quando Usar Geradores vs Listas

Use geradores quando:

  • Os dados são grandes demais para caber na memória
  • Você só precisa iterar sobre os valores uma vez
  • Está construindo pipelines de processamento de dados
  • Precisa de séries infinitas ou sob demanda
  • Performance de memória é prioridade

Use listas quando:

  • Precisa acessar elementos por índice (lista[3])
  • Precisa iterar múltiplas vezes sobre os mesmos dados
  • Precisa saber o tamanho com len()
  • O conjunto de dados é pequeno e cabe confortavelmente na memória
  • Precisa modificar elementos (append, insert, sort)

Uma regra prática: se você está apenas iterando com for e não precisa reusar os dados, um gerador é quase sempre a melhor escolha.

Conclusão

Iteradores e geradores são ferramentas fundamentais para escrever código Python eficiente e elegante. Os iteradores definem o protocolo que torna o for loop tão poderoso. Os geradores, com yield, simplificam a criação de iteradores e permitem processar dados sob demanda, economizando memória. Combinados com generator expressions e itertools, eles formam um arsenal completo para manipulação de dados em qualquer escala — de scripts simples a pipelines de dados corporativos.

Iteradores em outras linguagens: Rust é famosa por seus iteradores zero-cost — composições como .filter().map().collect() são otimizadas pelo compilador sem overhead de memória. Já Go introduziu recentemente o range-over-func, trazendo iteradores customizados para a linguagem de forma idiomática.

E

Equipe python.dev.br

Contribuidor do Python Brasil — Aprenda Python em Português