GERADORES a forma mais Pythônica de lidar com Fluxo de Dados
A Forma Mais Eficiente de LIdar Com Fluxo De Dados
Os Geradores são a forma mais Pythônica de ter o melhor dos Iteradores e das Funções: A concisão de uma função e a efiência de memória de um iterador. Assim como, são a maneira muito mais simples e concisa de criar um iterador.
Vamos pensar nos geradores como uma "função preguiçosa" (lazy function) ou uma "fábrica de iteradores"
- É uma função que, em vez de return um único valor e terminar, usa a palavra-chave yield para produzir (gerar) uma sequência de valores.
- Cada vez que yield é encontrado, a função "pausa" sua execução, "retorna" o valor gerado e mantém seue stado interno.
- Quando o próximo valor é solicitado (por exemplo, em um loop for ou com next()), a função retoma a execução de onde parou, continuando até o próximo yield ou até o fim da função.
- Quando a função geradora termina (não há mais yields ou há um return sem valor), ela levanta automaticamente uma SotpIteration, indicando o fim da sequência, exatamente como um iterador.
Uma grande vantagem é que não é preciso escrever uma classe complexa com __iter__() e __next__() para criar um iterador; o Python faz todo o trabalho de gerenciar o estado para você.
COMO USAR GERADORES?
Usa como se usa qualquer iterador:
- Chame a função geradora: Isso não executa o código dentro dela imediatament, mas retorna um objeto gerador.
- Itere sobre o objeto gerador: Use um loop for ou chame next() nele para começar a obter os valores.ValueError
Exemplo simples de um gerador
def meu_gerador():
yield 1
yield 2
yield 3
1. Chamar a função retorna um objeto gerador (que é um iterador!)
gen_obj = meu_gerador()
print(f" Tipo de objeto retornado: {type(gen_obj)}") # Tipo de objeto retornado: <class 'generator'>
2. Iterar sobre ele
print("Valores do gerador:")
for valor in gen_obj:
print(valor)
# SAÍDA
# Valores do gerador:
# 1
# 2
# 3
Se tentar iterar novamente, ele estará "esgotado"
for valor in gen_obj:
print(valor) # Não vai imprimir nada
COMPARAR FIBONACCI COM ITERADOR, GERADOR E FUNÇÃO
Vamos colocar os três lado a lado e medir a performance de memória e tempo para provar o ponto.
import time
from time import perf_counter
from pympler.asizeof import asizeof
LIMITE_FIBONACCI = 100000
ITERADOR
class FibonacciIterator:
def __init__(self, limite):
self.limite = limite
self.a = 0 # Primeiro elemento da sequência
self.b = 1 # Segundo elemento da sequência
self.contador = 0 # Para contar quantos números já foram gerados
def __iter__(self):
# Retorna o próprio objeto, pois ele já é um iterador
return self
def __next__(self):
# A lógica para gerar o próximo elemento da sequência
if self.contador < self.limite:
if self.contador == 0:
self.contador += 1
return self.a # Retorna 0 para o primeiro elemento
elif self.contador == 1:
self.contador +=1
return self.b # Retorna 1 para o segundo elemento
else:
proximo_fib = self.a + self.b
self.a = self.b
self.b = proximo_fib
self.contador += 1
return proximo_fib
else:
# QUando o limite é atigido, levantamos o StopIteration
raise StopIteration
fibo_iterador = FibonacciIterator(LIMITE_FIBONACCI)
print("\n### Teste 1: Classe Iterador ###")
inicio_iter = perf_counter()
for i, num in enumerate(fibo_iterador):
if i == 0:
print(f" Tamanho do objeto iterador (primeira iteração): {asizeof(fibo_iterador)} bytes")
print(f" Tamanho de um número gerado (ex: {num}): {asizeof(num)} bytes")
pass
fim_iter = perf_counter()
print(f" Tempo de execução (apenas iteração): {fim_iter - inicio_iter:.6f} segundos")
print(f" Consumo de memória para a sequência: Baixo e constante (não armazena tudo).")
print(f" (O iterador gerou {i+1} números)")
print(f" Tipo fibo_iterador: {type(fibo_iterador)}")
# SAÍDA
### Teste 1: Classe Iterador ###
# Tamanho do objeto iterador (primeira iteração): 640 bytes
# Tamanho de um número gerado (ex: 0): 32 bytes
# Tempo de execução (apenas iteração): 0.165058 segundos
# Consumo de memória para a sequência: Baixo e constante (não armazena tudo).
# (O iterador gerou 100000 números)
FUNÇÂO gera lista
def fibonacci_lista(limite):
a = 0
b = 1
contador = 0
sequencia = []
while contador < limite:
sequencia.append(a)
proximo_item = a + b
a = b
b = proximo_item
contador += 1
return sequencia
fib_list = fibonacci_lista(LIMITE_FIBONACCI)
print("\n### Teste 2: Função que Retorna Lista ###")
inicio_lista = perf_counter()
# Chamar a função que gera e retorna a lista completa
fib_list = fibonacci_lista(LIMITE_FIBONACCI)
fim_lista = perf_counter()
print(f" Tempo de execução (gerando e retornando lista): {fim_lista - inicio_lista:.6f} segundos")
print(f" Consumo de memória para a lista (pympler.asizeof): {asizeof(fib_list)} bytes")
print(f" (A lista contém {len(fib_list)} números)")
# Para ver alguns números e provar que a lista existe
print(f" Primeiros 5 números na lista: {fib_list[:5]}")
print(f" Tipo fibo_list: {type(fib_list)}") # Tipo de fib_list: <class 'list'>
# SAÍDA
# ### Teste 2: Função que Retorna Lista ###
# Tempo de execução (gerando e retornando lista): 0.474742 segundos
# Consumo de memória para a lista (pympler.asizeof): 466408808 bytes
# (A lista contém 100000 números)
# Primeiros 5 números na lista: [0, 1, 1, 2, 3]
FUNÇÂO GERADORA de Fibonacci (Nova!)
def fibonacci_gerador(LIMITE_FIBONACCI):
a, b = 0, 1
contador = 0
while contador < LIMITE_FIBONACCI:
yield a # Pausa aqui, retorna 'a', e mantém o estado
a, b = b, a + b
contador += 1
print("\n### Teste 3: Função Geradora ###")
# Chamar a função geradora para obter o objeto gerador
fib_gen = fibonacci_gerador(LIMITE_FIBONACCI)
inicio_gen = perf_counter()
# Iterar sobre o objeto gerador para obter os números
for i, num in enumerate(fib_gen):
if i == 0: # Medir o tamanho do objeto gerador uma vez no início
print(f" Tamanho do objeto gerador (primeira iteração): {asizeof(fib_gen)} bytes")
print(f" Tamanho de um número gerado (ex: {num}): {asizeof(num)} bytes")
# A memória consumida permanece constante (além do próprio número sendo processado)
pass # Apenas itera, não armazena para provar a eficiência de memória
fim_gen = perf_counter()
print(f" Tempo de execução (apenas iteração): {fim_gen - inicio_gen:.6f} segundos")
print(f" Consumo de memória para a sequência: Baixo e constante (não armazena tudo).")
print(f" (O gerador gerou {i+1} números)")
print(f" Tipo de fib_gen: {type(fib_gen)}") # Tipo de fib_gen: <class 'generator'>
# SAIDA
# Tamanho do objeto gerador (primeira iteração): 256 bytes
# Tamanho de um número gerado (ex: 0): 32 bytes
# Tempo de execução (apenas iteração): 0.129211 segundos
# Consumo de memória para a sequência: Baixo e constante (não armazena tudo).
# (O gerador gerou 100000 números)
RESULTADOS
MÉTODO | TEMPO | CONSUMO DE MEMÓRIA
ITERADOR | 0.167704 s | 32 bytes
Função Retorna Lista | 0.478771 s | 466408808 bytes
Função Geradora | 0.129211 s | 32 bytes
CONCLUSÃO
Essa comparação visual e numérica prova o ponto:
- Funções que geram listas são fáceis de usar quando você precisa de todos os dados de uma vez e o conjunto de dados é pequeno. A desvantagem é o alto consumo de memória para grandes volumes.
- Iteradores (via classes) oferecem eficiência de memória, mas exigem mais código (__init__, __iter__, __next__).
- Geradores (via funções yield) oferecem a mesma eficiência de memória dos iteradores, mas com uma sintaxe muito mais concisa e legível. Eles são a forma mais Pythônica de lidar com fluxos de dados "preguiçosos" e são a escolha ideal para a maioria dos casos onde você precisa de um iterador customizado.
É isso aí!
Fim da série Decoradores, Iteradores e Geradores espero que tenha gostado de ler esta série 😉