3.1 Conhecendo os generadores
Generadores são funções que retornam um objeto iterador. Esses iteradores geram valores conforme solicitados, o que permite lidar com grandes conjuntos de dados sem carregá-los completamente na memória.
Existem várias maneiras de criar generadores, e abaixo vamos discutir as mais populares.
Generadores baseados em funções
Generadores são criados usando a palavra-chave yield dentro de uma função. Quando a função com yield é chamada, ela retorna um objeto generador, mas não executa o código dentro da função imediatamente. Em vez disso, a execução é pausada na expressão yield e é retomada a cada chamada do método __next__() do objeto generador.
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # Saída: 1
print(next(counter)) # Saída: 2
print(next(counter)) # Saída: 3
print(next(counter)) # Saída: 4
print(next(counter)) # Saída: 5
Se uma função tem a instrução yield, o Python cria um objeto generador em vez de executar a função de maneira tradicional, que gerencia o estado da execução da função.
Expressões de generadores
As expressões de generadores são semelhantes às List Comprehensions, mas são criadas usando parênteses em vez de colchetes. Elas também retornam um objeto generador.
squares = (x ** 2 for x in range(10))
print(next(squares)) # Saída: 0
print(next(squares)) # Saída: 1
print(next(squares)) # Saída: 4
Qual das maneiras você prefere?
3.2 Vantagens dos generadores
Uso eficiente de memória
Generadores calculam valores sob demanda, o que permite lidar com grandes dados sem carregá-los completamente na memória. Isso torna os generadores uma escolha ideal para trabalhar com grandes conjuntos de dados ou fluxos de dados.
def large_range(n):
for i in range(n):
yield i
for value in large_range(1000000):
# Processamos os valores um por um
print(value)
Cálculos preguiçosos
Generadores realizam cálculos preguiçosos, o que significa que eles calculam valores somente quando necessário. Isso evita cálculos desnecessários e melhora o desempenho.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
Facilidade de sintaxe
Generadores proporcionam uma sintaxe fácil para criar iteradores, o que simplifica a escrita e leitura do código.
3.3 Uso de generadores
Exemplos de uso de generadores na biblioteca padrão
Muitas funções na biblioteca padrão do Python usam generadores. Por exemplo, a função range() retorna um objeto generador, que gera uma sequência de números.
for i in range(10):
print(i)
Sim, o mundo nunca mais será o mesmo.
Criando sequências infinitas
Generadores permitem criar sequências infinitas, que podem ser úteis em diversos cenários, como a geração de fluxos infinitos de dados.
def natural_numbers():
n = 1
while True:
yield n
n += 1
naturals = natural_numbers()
for _ in range(10):
print(next(naturals))
Uso de send() e close()
Objetos generadores suportam os métodos send() e close(), que permitem enviar valores de volta para o generador e encerrar sua execução.
def echo():
while True:
received = yield
print(received)
e = echo()
next(e) # Inicia o generador
e.send("Hello, world!") # Saída: Hello, world!
e.close()
3.4 Generadores na prática
Generadores e exceções
Generadores podem lidar com exceções, o que os torna uma ferramenta poderosa para a escrita de código mais robusto.
def controlled_execution():
try:
yield "Start"
yield "Working"
except GeneratorExit:
print("Generator closed")
gen = controlled_execution()
print(next(gen)) # Saída: Start
print(next(gen)) # Saída: Working
gen.close() # Saída: Generator closed
Trabalhar com exceções será abordado nas próximas palestras, mas acho útil saber que generadores lidam bem com elas.
Generadores aninhados
Generadores podem ser aninhados, o que permite criar estruturas iterativas complexas.
def generator1():
yield from range(3)
yield from "ABC"
for value in generator1():
print(value)
# Saída
0
1
2
A
B
C
Explicação:
yield from: Esta construção é usada para delegar parte das operações a outro generador, o que simplifica o código e melhora a legibilidade.
Generadores e desempenho
O uso de generadores pode melhorar significativamente o desempenho dos programas ao reduzir o uso de memória e executar iterações de forma mais eficiente.
Exemplo de comparação entre listas e generadores
import time
import sys
def memory_usage(obj):
return sys.getsizeof(obj)
n = 10_000_000
# Usando lista
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)
# Usando generador
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp) # Calculamos a soma para comparar os resultados
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)
print(f"Lista:")
print(f" Tempo: {list_time:.2f} seg")
print(f" Memória: {list_memory:,} bytes")
print(f"\nGenerador:")
print(f" Tempo: {gen_time:.2f} seg")
print(f" Memória: {gen_memory:,} bytes")
Lista:
Tempo: 0.62 seg
Memória: 89,095,160 bytes
Generador:
Tempo: 1.13 seg
Memória: 200 bytes
GO TO FULL VERSION