Geradores avançado
Esta aula aborda técnicas avançadas de geradores em Python, incluindo o uso de yield from para delegação, generator expressions para criação concisa, uma visão geral das coroutines clássicas e casos de uso práticos. O conteúdo inclui exemplos detalhados, boas práticas e exercícios com respostas.
Nesta aula, vamos aprofundar nosso conhecimento sobre geradores em Python, explorando recursos avançados que permitem escrever código mais eficiente e expressivo. Começaremos com o comando yield from, que facilita a delegação de geração para outro iterável, e depois veremos as generator expressions, uma forma concisa de criar geradores. Em seguida, daremos uma visão geral das coroutines clássicas, que são geradores que também podem receber valores, e finalizaremos com casos de uso práticos que demonstram a aplicação desses conceitos em problemas reais.
Dominar geradores avançados é essencial para escrever código Python idiomático, eficiente em memória e que aproveita ao máximo o modelo de execução lazy da linguagem. Ao longo da aula, você verá como esses recursos se complementam e como podem ser usados para resolver problemas complexos de forma elegante.
yield from
O yield from é um recurso introduzido no Python 3.3 que permite delegar a geração de valores para outro iterável (como uma lista, tupla, gerador ou qualquer objeto iterável). Em vez de escrever um loop for para iterar sobre um sub-iterador e fazer yield de cada item, você pode simplesmente usar yield from sub_iteravel. Isso torna o código mais limpo e também melhora o desempenho, pois o Python otimiza a delegação.
Além de simplificar a delegação, yield from também lida corretamente com a comunicação bidirecional entre o gerador e o chamador, o que é essencial para coroutines (veremos adiante). Isso inclui o envio de valores via send() e o tratamento de exceções via throw().
Vejamos um exemplo simples:
def sub_generator():
yield 1
yield 2
yield 3
def main_generator():
yield 0
yield from sub_generator()
yield 4
print(list(main_generator())) # Saída: [0, 1, 2, 3, 4]
Neste exemplo, main_generator primeiro produz 0, depois delega para sub_generator que produz 1, 2 e 3, e finalmente produz 4. O uso de yield from evita um loop explícito e torna o código mais legível.
Generator expressions
Generator expressions são uma forma concisa de criar geradores, semelhante às list comprehensions, mas com parênteses em vez de colchetes. Elas são avaliadas de forma preguiçosa (lazy), ou seja, os itens são gerados um a um conforme são necessários, economizando memória. A sintaxe básica é: (expressão for item in iterável if condição).
Elas são ideais quando você precisa de um iterável temporário e não deseja armazenar todos os elementos em memória. Por exemplo, para somar os quadrados de números de 1 a 10, você pode usar:
soma = sum(x**2 for x in range(1, 11))
print(soma) # Saída: 385
Note que aqui os parênteses são opcionais quando a generator expression é o único argumento da função. Isso torna o código ainda mais limpo. Generator expressions também podem ser usadas em outras funções que aceitam iteráveis, como any(), all(), max(), min(), etc.
É importante distinguir generator expressions de list comprehensions: enquanto a list comprehension cria uma lista completa na memória, a generator expression cria um gerador que produz os itens sob demanda. Isso é particularmente útil ao trabalhar com grandes volumes de dados.
Coroutines clássicas (visão geral)
Coroutines são uma generalização de geradores que permitem não apenas produzir valores, mas também receber valores do chamador. Em Python, isso é feito através do método send(), que envia um valor para dentro do gerador, e do yield que atua como uma expressão que captura o valor enviado. As coroutines clássicas (antes do async/await) eram implementadas usando geradores com send() e yield.
Um exemplo simples de coroutine:
def coroutine():
total = 0
while True:
valor = yield total
if valor is None:
break
total += valor
# Uso
coro = coroutine()
next(coro) # Primeiro yield para iniciar
print(coro.send(10)) # Saída: 10
print(coro.send(20)) # Saída: 30
coro.close() # Encerra
O método send() envia um valor para o gerador, que é recebido pela expressão yield. O gerador então processa o valor e pode produzir um resultado. O next() é equivalente a send(None) e é usado para iniciar o gerador. Coroutines permitem criar pipelines de processamento de dados, onde cada estágio pode receber e enviar valores.
Embora o async/await tenha substituído as coroutines clássicas para programação assíncrona, entender o conceito é fundamental para compreender como o Python lida com concorrência e para ler código legado.
Casos de uso
Geradores avançados são amplamente utilizados em diversas situações práticas. Um dos casos mais comuns é o processamento de arquivos grandes, onde ler o arquivo linha por linha com um gerador evita carregar todo o conteúdo na memória. Por exemplo:
def ler_linhas(nome_arquivo):
with open(nome_arquivo, 'r') as arquivo:
for linha in arquivo:
yield linha.strip()
for linha in ler_linhas('dados.txt'):
print(linha)
Outro caso é a criação de pipelines de processamento de dados, onde cada estágio é um gerador que transforma os dados e passa adiante. Por exemplo, para processar uma sequência de números, você pode ter geradores para filtrar, transformar e acumular:
def numeros():
for i in range(10):
yield i
def pares(iteravel):
for x in iteravel:
if x % 2 == 0:
yield x
def ao_quadrado(iteravel):
for x in iteravel:
yield x**2
pipeline = ao_quadrado(pares(numeros()))
print(list(pipeline)) # Saída: [0, 4, 16, 36, 64]
Também são úteis em algoritmos que precisam gerar sequências infinitas, como a sequência de Fibonacci, sem consumir memória indefinidamente:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib)) # Imprime os 10 primeiros números de Fibonacci
Além disso, geradores são usados em implementações de streams de dados, em frameworks de web scraping para paginação, e em qualquer situação onde você precise de uma sequência de valores que pode ser produzida sob demanda.
Boas práticas e observações finais
Ao usar geradores avançados, é importante ter em mente algumas boas práticas. Primeiro, sempre que possível, prefira generator expressions a list comprehensions quando o resultado for usado uma única vez ou quando a lista for grande demais para caber na memória. Segundo, use yield from para delegar a geração de valores a sub-geradores, pois isso melhora a legibilidade e o desempenho. Terceiro, ao implementar coroutines, lembre-se de iniciar o gerador com next() ou send(None) antes de enviar valores. Finalmente, considere usar a biblioteca itertools que oferece várias funções úteis para trabalhar com iteradores e geradores.
Em resumo, os geradores avançados são ferramentas poderosas que permitem escrever código eficiente, elegante e que lida bem com grandes volumes de dados. Praticar com exemplos reais é a melhor forma de dominá-los.
Exercícios
- Escreva uma função geradora que receba uma lista de listas e produza todos os elementos em ordem, usando
yield from. - Dada uma lista de números, crie uma generator expression que produza apenas os números pares e, em seguida, calcule a soma deles.
- Implemente uma coroutine que receba uma sequência de números e devolva a média móvel (a média dos últimos N números). Use
send()para enviar os números. - Crie um gerador que produza os primeiros N números primos, usando um algoritmo eficiente.
- Escreva uma função que leia um arquivo grande e conte quantas linhas contêm uma determinada palavra, usando um gerador para ler linha a linha.
def achatar(lista):
for sublista in lista:
yield from sublista
lista = [[1, 2], [3, 4], [5]]
print(list(achatar(lista))) # [1, 2, 3, 4, 5]
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pares = (n for n in numeros if n % 2 == 0)
soma = sum(pares)
print(soma) # 30
def media_movel(tamanho):
valores = []
while True:
novo = yield None
valores.append(novo)
if len(valores) > tamanho:
valores.pop(0)
media = sum(valores) / len(valores)
# Envia a média de volta
try:
yield media
except GeneratorExit:
break
mm = media_movel(3)
next(mm) # Inicializa
print(mm.send(10)) # 10.0
print(mm.send(20)) # 15.0
print(mm.send(30)) # 20.0
print(mm.send(40)) # 30.0
mm.close()
def primos(n):
count = 0
num = 2
while count < n:
for i in range(2, int(num**0.5) + 1):
if num % i == 0:
break
else:
yield num
count += 1
num += 1
print(list(primos(10))) # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
def contar_palavra(nome_arquivo, palavra):
contador = 0
with open(nome_arquivo, 'r') as arquivo:
for linha in arquivo:
if palavra in linha:
contador += 1
return contador
# Uso: contar_palavra('texto.txt', 'Python')