36. Geradores e Iteração Preguiçosa
No Capítulo 35, aprendemos como a iteração funciona em Python por meio de iteráveis e iteradores. Vimos que iteradores retornam valores um de cada vez quando solicitado, o que permite que o Python processe sequências sem carregar tudo na memória de uma só vez. Agora vamos explorar geradores, a forma mais elegante e prática do Python de criar iteradores.
Geradores são funções que conseguem pausar e retomar a execução, produzindo valores um de cada vez conforme são solicitados, em vez de calcular todos os valores antecipadamente e armazená-los na memória. Essa abordagem—chamada de avaliação preguiçosa (lazy evaluation)—significa que os valores são gerados só quando necessário, tornando isso um dos recursos mais poderosos do Python para escrever código eficiente em memória.
36.1) O que são geradores e por que eles são úteis
36.1.1) O problema de criar listas grandes
Vamos começar entendendo o problema que os geradores resolvem. Suponha que você precise processar uma sequência de um milhão de números. Aqui está a abordagem tradicional usando uma lista:
# Criando uma lista de um milhão de quadrados
def get_squares_list(n):
"""Return a list of squares from 0 to n-1."""
squares = []
for i in range(n):
squares.append(i * i)
return squares
# Isso cria uma lista com 1.000.000 de números na memória
numbers = get_squares_list(1_000_000)
print(f"First five squares: {numbers[:5]}") # Output: First five squares: [0, 1, 4, 9, 16]Essa abordagem tem um problema significativo: ela cria e armazena todos os um milhão de números na memória de uma vez, mesmo que você só precise processá-los um de cada vez. Para conjuntos de dados maiores ou cálculos mais complexos, isso pode consumir quantidades enormes de memória ou até travar o seu programa.
36.1.2) Apresentando geradores: calculando valores sob demanda
Um gerador é um tipo especial de função que produz valores um de cada vez, somente quando solicitado. Em vez de construir e retornar uma lista completa, um gerador calcula cada valor conforme necessário e “lembra” onde parou entre chamadas.
Aqui está a mesma funcionalidade implementada como um gerador:
# Criando um gerador de quadrados
def get_squares_generator(n):
"""Generate squares from 0 to n-1, one at a time."""
for i in range(n):
yield i * i # yield pausa a função e retorna um valor
# Isso cria um objeto gerador, não uma lista
squares_gen = get_squares_generator(1_000_000)
print(squares_gen) # Output: <generator object get_squares_generator at 0x...>
# Pegue valores um de cada vez
print(next(squares_gen)) # Output: 0
print(next(squares_gen)) # Output: 1
print(next(squares_gen)) # Output: 4O gerador não calcula todos os um milhão de quadrados antecipadamente. Em vez disso, ele calcula cada quadrado apenas quando você chama next() nele. Entre chamadas, o gerador “pausa” e lembra seu estado (o valor atual de i).
36.1.3) Eficiência de memória: a principal vantagem
A diferença de memória entre listas e geradores fica dramática com conjuntos de dados grandes. Vamos comparar:
import sys
# Abordagem com lista: armazena todos os valores
def squares_list(n):
return [i * i for i in range(n)]
# Abordagem com gerador: calcula valores sob demanda
def squares_generator(n):
for i in range(n):
yield i * i
# Compare o uso de memória para 100.000 números
list_result = squares_list(100_000)
gen_result = squares_generator(100_000)
print(f"List size in memory: {sys.getsizeof(list_result):,} bytes")
# Output: List size in memory: 800,984 bytes (actual size may vary)
print(f"Generator size in memory: {sys.getsizeof(gen_result)} bytes")
# Output: Generator size in memory: 200 bytes (actual size may vary)A lista consome mais de 800 KB de memória, enquanto o gerador usa apenas 200 bytes—independentemente de quantos valores ele eventualmente vai produzir. O gerador armazena apenas o estado da função (o valor atual de i e onde retomar), não a sequência real de valores.
36.1.4) Quando geradores são úteis
Geradores se destacam em vários cenários comuns:
Processamento de arquivos grandes:
def read_large_file(filename):
"""Generate lines from a file one at a time."""
with open(filename, 'r') as file:
for line in file:
yield line.strip()
# Processe um arquivo de log enorme sem carregar tudo na memória
for line in read_large_file('huge_log.txt'):
if 'ERROR' in line:
print(line)Sequências infinitas:
def fibonacci():
"""Generate Fibonacci numbers indefinitely."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Gere números de Fibonacci para sempre (ou até você parar de pedir)
fib = fibonacci()
print([next(fib) for _ in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]36.1.5) Geradores são iteradores
Como aprendemos no Capítulo 35, geradores na verdade são um tipo especial de iterador. Eles implementam automaticamente o protocolo de iterador (__iter__() e __next__()), e é por isso que funcionam de forma integrada com loops for:
def countdown(n):
"""Generate countdown from n to 1."""
while n > 0:
yield n
n -= 1
# Geradores funcionam diretamente em loops for
for num in countdown(5):
print(num)
# Output:
# 5
# 4
# 3
# 2
# 1Quando você usa um gerador em um loop for, o Python chama automaticamente next() nele repetidamente até o gerador se esgotar (levantar StopIteration).
36.2) Criando funções geradoras com yield
36.2.1) A instrução yield: pausando e retomando
A instrução yield é o que torna uma função um gerador. Quando o Python encontra yield, ele faz algo especial: em vez de retornar um valor e encerrar a função, ele pausa a função e retorna o valor. Na próxima vez que você chamar next() no gerador, a execução retoma logo após a instrução yield.
Aqui está um exemplo simples que demonstra esse comportamento de pausar e retomar:
def simple_generator():
"""Demonstrate how yield pauses execution."""
print("Starting generator")
yield 1
print("Resuming after first yield")
yield 2
print("Resuming after second yield")
yield 3
print("Generator finished")
gen = simple_generator()
print("Created generator")
# Output:
# Created generator
print(f"First value: {next(gen)}")
# Output:
# Starting generator
# First value: 1
print(f"Second value: {next(gen)}")
# Output:
# Resuming after first yield
# Second value: 2
print(f"Third value: {next(gen)}")
# Output:
# Resuming after second yield
# Third value: 3
try:
next(gen)
except StopIteration:
print("Generator exhausted - no more values")
# Output:
# Generator finished
# Generator exhausted - no more valuesPerceba como a execução da função é intercalada com as chamadas a next(). Cada yield pausa a função, e cada next() a retoma de onde ela parou.
36.2.2) Estado do gerador: lembrando variáveis locais
Geradores lembram todas as variáveis locais entre yields. Isso os torna úteis para manter estado ao longo de várias chamadas:
def counter(start=0):
"""Generate sequential numbers starting from start."""
current = start
while True:
yield current
current += 1
# O gerador lembra 'current' entre yields
count = counter(10)
print(next(count)) # Output: 10
print(next(count)) # Output: 11
print(next(count)) # Output: 12
# Cada gerador tem seu próprio estado independente
count1 = counter(0)
count2 = counter(100)
print(next(count1)) # Output: 0
print(next(count2)) # Output: 100
print(next(count1)) # Output: 1
print(next(count2)) # Output: 101A variável current é preservada a cada vez que o gerador pausa em um yield e retoma na próxima chamada de next(). Isso permite que o gerador continue contando a partir do último valor. Cada instância de gerador mantém seu próprio estado independente.
36.2.3) Usando yield em loops: o padrão mais comum
O uso mais comum de geradores é produzir valores com yield dentro de um loop. Esse padrão gera uma sequência de valores:
def even_numbers(start, end):
"""Generate even numbers in the given range."""
current = start if start % 2 == 0 else start + 1
while current <= end:
yield current
current += 2
# Use o gerador
evens = even_numbers(1, 20)
print(list(evens))
# Output: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]Cada iteração do loop produz um valor e, depois, continua para a próxima iteração quando next() é chamado novamente.
36.2.4) Múltiplas instruções yield
Um gerador pode ter várias instruções yield em pontos diferentes do código. A execução passa por elas em ordem:
def process_data(data):
"""Generate processed data with status messages."""
yield "Starting processing..."
cleaned = [item.strip().lower() for item in data]
yield f"Cleaned {len(cleaned)} items"
unique = list(set(cleaned))
yield f"Found {len(unique)} unique items"
for item in sorted(unique):
yield item
# Processe alguns dados
data = [" Apple ", "Banana", "apple", "Cherry", "BANANA"]
processor = process_data(data)
for result in processor:
print(result)
# Output:
# Starting processing...
# Cleaned 5 items
# Found 3 unique items
# apple
# banana
# cherryEsse padrão é útil para geradores que precisam fazer trabalho de preparação, produzir informações de status e então produzir os dados reais.
36.3) Expressões geradoras vs compreensões de lista
36.3.1) Apresentando expressões geradoras
No Capítulo 34, aprendemos sobre compreensões de lista—uma forma concisa de criar listas. Expressões geradoras usam uma sintaxe quase idêntica, mas criam geradores em vez de listas.
Uma expressão geradora é essencialmente um jeito compacto de escrever uma função geradora simples. Compare estas duas abordagens equivalentes:
# Função geradora
def squares_function(n):
for x in range(n):
yield x * x
# Expressão geradora - faz a mesma coisa
squares_expression = (x * x for x in range(10))
# Ambas criam objetos geradores
gen1 = squares_function(10)
gen2 = squares_expression
print(type(gen1)) # Output: <class 'generator'>
print(type(gen2)) # Output: <class 'generator'>
# Ambas produzem os mesmos valores
print(list(squares_function(10))) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(list(squares_expression)) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]A sintaxe é quase idêntica à de compreensões de lista. As diferenças são: usar parênteses () em vez de colchetes [], e enquanto compreensões de lista criam listas, expressões geradoras criam geradores:
# Compreensão de lista - cria a lista inteira na memória
squares_list = [x * x for x in range(10)]
print(squares_list)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# Expressão geradora - cria um objeto gerador
squares_gen = (x * x for x in range(10))
print(squares_gen)
# Output: <generator object <genexpr> at 0x...>
# Converta para lista para ver os valores
print(list(squares_gen))
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Expressões geradoras oferecem a mesma sintaxe concisa que compreensões de lista, mas com a eficiência de memória dos geradores.
36.3.2) Comparação de memória: quando isso importa
Para sequências pequenas, a diferença de memória entre compreensões de lista e expressões geradoras é desprezível. Mas para sequências grandes, ela se torna significativa:
import sys
# Sequência pequena - diferença mínima
small_list = [x for x in range(100)]
small_gen = (x for x in range(100))
print(f"Small list: {sys.getsizeof(small_list)} bytes")
# Output: Small list: 920 bytes (actual size may vary)
print(f"Small generator: {sys.getsizeof(small_gen)} bytes")
# Output: Small generator: 192 bytes (actual size may vary)
# Sequência grande - enorme diferença
large_list = [x for x in range(1_000_000)]
large_gen = (x for x in range(1_000_000))
print(f"Large list: {sys.getsizeof(large_list):,} bytes")
# Output: Large list: 8,448,728 bytes (actual size may vary)
print(f"Large generator: {sys.getsizeof(large_gen)} bytes")
# Output: Large generator: 192 bytes (actual size may vary)O tamanho do gerador permanece constante, independentemente de quantos valores ele vai produzir—ele só armazena a expressão e o estado atual. Já a lista precisa armazenar todos os valores na memória, e é por isso que seu tamanho cresce proporcionalmente ao número de elementos.
36.3.3) Expressões geradoras em chamadas de função
Expressões geradoras ficam particularmente elegantes quando passadas diretamente para funções que consomem iteráveis. Você pode omitir os parênteses extras quando uma expressão geradora é o único argumento:
# Calcule a soma dos quadrados sem criar uma lista
total = sum(x * x for x in range(100)) # Note: no extra parentheses needed
print(total)
# Output: 328350
# Encontre o máximo de valores transformados
numbers = [1, 2, 3, 4, 5]
max_square = max(x * x for x in numbers)
print(max_square)
# Output: 25
# Verifique se algum valor atende a uma condição
data = [10, 15, 20, 25, 30]
has_large = any(x > 100 for x in data)
print(has_large)
# Output: FalseEsse padrão é eficiente em memória e legível. Funções como sum(), max(), min(), any() e all() processam o gerador um valor por vez, sem nunca criar uma lista intermediária.
36.3.4) Filtrando com expressões geradoras
Expressões geradoras suportam a mesma lógica condicional que compreensões de lista:
# Filtre números pares
numbers = range(20)
evens = (x for x in numbers if x % 2 == 0)
print(list(evens))
# Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# Transforme e filtre
words = ["hello", "world", "python", "programming"]
long_upper = (word.upper() for word in words if len(word) > 5)
print(list(long_upper))
# Output: ['PYTHON', 'PROGRAMMING']36.3.5) Quando expressões geradoras não são suficientes
Expressões geradoras são concisas e elegantes, mas têm limitações. Use funções geradoras quando você precisar de:
Lógica complexa:
# Complexo demais para uma expressão geradora
def process_log_lines(filename):
"""Process log file with complex logic."""
with open(filename, 'r') as file:
for line in file:
line = line.strip()
if not line or line.startswith('#'):
continue # Pule linhas vazias e comentários
parts = line.split('|')
if len(parts) >= 3:
timestamp, level, message = parts[0], parts[1], parts[2]
if level in ('ERROR', 'CRITICAL'):
yield {
'timestamp': timestamp,
'level': level,
'message': message
}Múltiplos yields ou estado:
# Expressão geradora não consegue manter estado entre iterações
def running_total(numbers):
"""Generate running total of numbers."""
total = 0
for num in numbers:
total += num
yield total
numbers = [1, 2, 3, 4, 5]
print(list(running_total(numbers)))
# Output: [1, 3, 6, 10, 15]Tratamento de erros:
# Expressão geradora não consegue tratar exceções
def safe_divide(numbers, divisor):
"""Generate division results, handling errors."""
for num in numbers:
try:
yield num / divisor
except ZeroDivisionError:
yield float('inf')36.4) Quando usar geradores em vez de listas
36.4.1) Grandes conjuntos de dados: o principal caso de uso
O motivo mais convincente para usar geradores é quando você trabalha com grandes quantidades de dados. Se você estiver processando milhões de registros, geradores podem ser a diferença entre um programa que roda de forma suave e um que trava.
Abordagem ruim - Carregar o arquivo inteiro na memória:
# NÃO FAÇA ISSO com arquivos grandes
def count_errors_bad(filename):
"""Carrega o arquivo inteiro na memória - vai travar com arquivos grandes."""
with open(filename, 'r') as file:
lines = file.readlines() # Carrega o arquivo INTEIRO na memória
error_count = 0
for line in lines:
if 'ERROR' in line:
error_count += 1
return error_count
# Se o arquivo tiver 10 GB, isso tenta carregar 10 GB na memória!Boa abordagem - Usar um gerador:
def read_log_lines(filename):
"""Generate lines from a log file one at a time."""
with open(filename, 'r') as file:
for line in file:
yield line.strip()
def count_errors_good(filename):
"""Count errors without loading entire file into memory."""
error_count = 0
for line in read_log_lines(filename):
if 'ERROR' in line:
error_count += 1
return error_count
# Isso funciona de forma eficiente até com arquivos de log de vários gigabytes
# porque ele só mantém uma linha na memória por vez
count = count_errors_good('huge_application.log')
print(f"Found {count} errors")A abordagem com gerador processa uma linha por vez, então o uso de memória permanece constante, independentemente do tamanho do arquivo. Um arquivo de 10 GB usa a mesma quantidade de memória que um arquivo de 10 KB.
36.4.2) Sequências infinitas ou de comprimento desconhecido
Geradores são perfeitos para sequências em que você não sabe o comprimento com antecedência ou em que a sequência é conceitualmente infinita:
def user_input_stream():
"""Generate user inputs until they type 'quit'."""
while True:
user_input = input("Enter a number (or 'quit'): ")
if user_input.lower() == 'quit':
break
try:
yield int(user_input)
except ValueError:
print("Invalid number, try again")
# Processe entradas do usuário conforme chegam
total = 0
count = 0
for number in user_input_stream():
total += number
count += 1
print(f"Running average: {total / count:.2f}")Você não consegue criar uma lista de comprimento desconhecido, mas um gerador lida com isso naturalmente.
36.4.3) Transformações encadeadas: construindo pipelines de dados
Quando você precisa aplicar várias transformações aos dados, geradores permitem encadear operações sem criar listas intermediárias:
# Transforme números por vários estágios
def generate_numbers(n):
"""Generate numbers from 1 to n."""
for i in range(1, n + 1):
yield i
def square_numbers(numbers):
"""Generate squares of input numbers."""
for num in numbers:
yield num * num
def keep_even(numbers):
"""Generate only even numbers."""
for num in numbers:
if num % 2 == 0:
yield num
# Encadeie geradores - nenhuma lista intermediária é criada
numbers = generate_numbers(10)
squared = square_numbers(numbers)
even_squares = keep_even(squared)
# Processe os resultados
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]Cada estágio processa um valor por vez, passando-o para o próximo estágio. Isso é eficiente em memória e permite que você processe conjuntos de dados maiores do que a RAM disponível.
Sem geradores, você precisaria de listas intermediárias:
# Abordagem sem gerador - cria listas intermediárias
numbers = list(range(1, 11)) # [1, 2, 3, ..., 10]
squared = [n * n for n in numbers] # [1, 4, 9, ..., 100]
even_squares = [n for n in squared if n % 2 == 0] # [4, 16, 36, 64, 100]
# Com geradores - sem listas intermediárias
numbers = (i for i in range(1, 11))
squared = (n * n for n in numbers)
even_squares = (n for n in squared if n % 2 == 0)
print(list(even_squares))
# Output: [4, 16, 36, 64, 100]Para um pipeline com três estágios processando um milhão de itens, a abordagem com lista criaria três listas de um milhão de itens cada. A abordagem com gerador mantém apenas um valor na memória por vez.
36.4.4) Quando listas são melhores do que geradores
Apesar das vantagens, geradores nem sempre são a escolha certa. Use listas quando você precisar de:
Múltiplas iterações:
# Lista - dá para iterar várias vezes
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
print(max(numbers)) # Output: 5 (works fine)
# Gerador - só dá para iterar uma vez
numbers_gen = (x for x in range(1, 6))
print(sum(numbers_gen)) # Output: 15
print(max(numbers_gen)) # Output: ValueError: max() iterable argument is emptySe você precisa processar os mesmos dados várias vezes, use uma lista.
Acesso aleatório:
# Precisa acessar elementos por índice - use uma lista
students = ['Alice', 'Bob', 'Charlie', 'Diana']
print(students[2]) # Output: Charlie
# Geradores não suportam indexação
students_gen = (name for name in students)
# students_gen[2] # ERROR: 'generator' object is not subscriptableInformação de comprimento:
# Precisa saber o tamanho - use uma lista
data = [1, 2, 3, 4, 5]
print(f"Processing {len(data)} items")
# Geradores não têm um tamanho
data_gen = (x for x in data)
# len(data_gen) # ERROR: object of type 'generator' has no len()Conjuntos de dados pequenos:
# Para conjuntos de dados pequenos, listas são ok e mais convenientes
small_data = [x * 2 for x in range(10)]
# A economia de memória de um gerador não é significativa aqui
# e a lista é mais flexível36.4.5) Guia prático de decisão
Aqui está um guia prático para escolher entre geradores e listas:
Use Geradores Quando:
- Processar arquivos ou conjuntos de dados grandes
- Trabalhar com streams de dados ou entrada do usuário
- Construir pipelines de processamento de dados
- Eficiência de memória for importante
- Você só precisar iterar uma vez
- A sequência for infinita ou muito longa
Use Listas Quando:
- O conjunto de dados for pequeno (normalmente < 10.000 itens)
- Você precisar iterar várias vezes
- Você precisar de acesso aleatório por índice
- Você precisar saber o comprimento
- Você precisar passar os dados para código que espera uma lista
36.4.6) Convertendo entre geradores e listas
Você pode converter facilmente entre geradores e listas quando necessário:
# Gerador para lista
numbers_gen = (x * 2 for x in range(5))
numbers_list = list(numbers_gen)
print(numbers_list)
# Output: [0, 2, 4, 6, 8]
# Lista para gerador (usando expressão geradora)
numbers_list = [1, 2, 3, 4, 5]
numbers_gen = (x for x in numbers_list)Essa flexibilidade significa que você pode começar com um gerador para ganhar eficiência e converter para uma lista apenas quando precisar de recursos específicos de lista:
# Comece com um gerador para eficiência de memória
numbers = (x for x in range(1, 1001))
filtered = (x for x in numbers if x % 7 == 0)
# Converta para lista quando você precisar de múltiplas iterações
multiples_of_seven = list(filtered)
# Agora você pode usar recursos de lista
print(f"Count: {len(multiples_of_seven)}")
# Output: Count: 142
print(f"First: {multiples_of_seven[0]}")
# Output: First: 7
print(f"Last: {multiples_of_seven[-1]}")
# Output: Last: 994
# Dá para iterar várias vezes
total = sum(multiples_of_seven)
average = total / len(multiples_of_seven)
print(f"Average: {average:.1f}")
# Output: Average: 500.5Geradores são um dos recursos mais elegantes do Python para escrever código eficiente em memória. Eles permitem que você processe conjuntos de dados grandes, construa pipelines de dados e trabalhe com sequências infinitas—tudo isso mantendo seu código limpo e legível. Conforme você ganhar experiência, vai desenvolver uma intuição sobre quando geradores são a ferramenta certa para o trabalho.