curso
Geradores Python: Aumentar o desempenho e simplificar o código
Imagine que você está trabalhando em um projeto de ciência de dados e sua tarefa é processar um conjunto de dados tão grande que carregá-lo na memória trava sua máquina. Ou você está lidando com uma sequência infinita, como um fluxo de dados ao vivo, em que não é possível armazenar tudo simultaneamente. Esses são os tipos de desafios que fazem com que os cientistas de dados peguem a cafeteira e, às vezes, o botão de reinicialização.
Neste artigo, aprenderemos sobre os geradores Python e como você pode usá-los para simplificar seu código. Essa ideia requer alguma prática, portanto, se você for iniciante em Python e se perder um pouco neste artigo, experimente nosso curso Introduction to Python para criar uma base sólida.
Aprenda Python do zero
O que são geradores Python?
Em sua essência, os geradores Python são um tipo especial de função ou até mesmo uma expressão compacta que produz uma sequência de valores de forma preguiçosa. Pense nos geradores como uma correia transportadora em uma fábrica: Em vez de empilhar todos os produtos em um só lugar e ficar sem espaço, você processa cada item à medida que ele entra na linha. Isso torna os geradores eficientes em termos de memória e uma extensão natural do protocolo iterator
do Python, que sustenta muitas das ferramentas integradas do Python, como loops for e compreensões.
A mágica por trás dos geradores está na palavra-chave yield
. Ao contrário do return,
, que produz um único valor e sai da função, o yield
produz um valor, pausa a execução da função e salva seu estado. Quando o gerador é chamado novamente, ele continua de onde parou.
Por exemplo, imagine que você esteja lendo um arquivo de log enorme, linha por linha. Um gerador pode processar cada linha lida sem carregar o arquivo inteiro na memória. Essa "avaliação preguiçosa" diferencia os geradores das funções tradicionais e os torna uma ferramenta essencial para tarefas sensíveis ao desempenho.
Um exemplo básico de gerador Python
Vamos praticar um pouco para você pegar o jeito da ideia. Aqui está uma função geradora que produz os primeiros n
números inteiros.
def generate_integers(n):
for i in range(n):
yield i # Pauses here and returns i
# Using the generator
for num in generate_integers(5):
print(num)
0
1
2
3
4
Criei um visual para ajudar você a ver o que está acontecendo nos bastidores:
Sintaxe e padrões do gerador Python
Os geradores podem ser implementados de várias maneiras. Dito isso, há duas formas principais: funções geradoras e expressões geradoras.
Funções do gerador
Uma função geradora é definida como uma função normal, mas usa a palavra-chave yield
em vez de return.
. Quando chamada, ela retorna um objeto gerador que pode ser iterado.
def count_up_to(n):
count = 1
while count <= n:
yield count
count += 1
# Using the generator
counter = count_up_to(5)
for num in counter:
print(num)
1
2
3
4
5
No exemplo acima, podemos ver que, quando a função count_up_to
é chamada, ela retorna um objeto gerador. Cada vez que o loop for solicita um valor, a função é executada até atingir yield
, produzindo o valor atual de count
e preservando seu estado entre as iterações para que possa retomar exatamente de onde parou.
Expressões do gerador
As expressões de gerador são uma maneira compacta de criar geradores. Eles são semelhantes às compreensões de lista, mas com parênteses em vez de colchetes.
# List comprehension (eager evaluation)
squares_list = [x**2 for x in range(5)] # [0, 1, 4, 9, 16]
# Generator expression (lazy evaluation)
squares_gen = (x**2 for x in range(5))
# Using the generator
for square in squares_gen:
print(square)
0
1
4
9
16
Então, qual é a diferença entre uma compreensão de lista e uma expressão de gerador? A compreensão de lista cria a lista inteira na memória, enquanto a expressão geradora produz valores um por vez, economizando memória. Se não estiver familiarizado com as compreensões de lista, você pode ler sobre elas em nosso Tutorial de compreensão de lista do Python.
Gerador Python vs. iterador
Os iteradores tradicionais em Python exigiam classes com métodos __iter__()
e __next__()
explícitos, que envolviam muito boilerplate e gerenciamento manual de estado, enquanto as funções geradoras simplificam o processo preservando automaticamente o estado e eliminando a necessidade desses métodos, conforme demonstrado por uma função simples que produz o quadrado de cada número até n
.
Por que usamos geradores Python
Ao explicar o que são geradores Python, também transmiti a você um pouco da ideia de por que eles são usados. Nesta seção, quero entrar em um pouco mais de detalhes. Como os geradores não são apenas um recurso sofisticado do Python, eles realmente resolvem problemas reais.
Eficiência de memória
Ao contrário das listas ou matrizes, que armazenam todos os seus elementos na memória simultaneamente, os geradores produzem valores em tempo real, de modo que mantêm apenas um item na memória por vez.
Por exemplo, considere a diferença entre range()
e xrange()
do Python 2:
-
range()
criou uma lista na memória, o que pode ser problemático para intervalos grandes. -
xrange()
agiu como um gerador, produzindo valores preguiçosamente.
Como o comportamento do xrange()
era mais útil, agora, no Python 3, o range()
também se comporta como um gerador, de modo que evita a sobrecarga de memória de armazenar todos os valores simultaneamente.
Para mostrar a ideia, vamos comparar o uso da memória ao gerar uma sequência de 10 milhões de números:
import sys
# Using a list
numbers_list = [x for x in range(10_000_000)]
print(f"Memory used by list: {sys.getsizeof(numbers_list) / 1_000_000:.2f} MB")
# Using a generator
numbers_gen = (x for x in range(10_000_000))
print(f"Memory used by generator: {sys.getsizeof(numbers_gen)} bytes")
Memory used by list: 89.48 MB
Memory used by the generator: 112 bytes
Como você pode ver, o gerador não usa quase nenhuma memória em comparação com a lista, e essa diferença é significativa.
Melhorias no desempenho
Graças à avaliação preguiçosa, os valores são computados somente quando necessário. Isso significa que você pode começar a processar os dados imediatamente, sem esperar que toda a sequência seja gerada.
Por exemplo, imagine somar os quadrados dos primeiros 1 milhão de números:
# Using a list (eager evaluation)
sum_of_squares_list = sum([x**2 for x in range(1_000_000)])
# Using a generator (lazy evaluation)
sum_of_squares_gen = sum(x**2 for x in range(1_000_000))
Embora ambas as abordagens apresentem o mesmo resultado, a versão do gerador evita a criação de uma lista enorme e, portanto, obtemos o resultado mais rapidamente.
Simplicidade e facilidade de leitura
Os geradores simplificam a implementação de iteradores, eliminando o código padrão. Compare um iterador baseado em classe com uma função geradora:
Aqui está o iterador baseado em classe:
class SquaresIterator:
def __init__(self, n):
self.n = n
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.n:
raise StopIteration
result = self.current ** 2
self.current += 1
return result
# Usage
squares = SquaresIterator(5)
for square in squares:
print(square)
Aqui está a função do gerador:
def squares_generator(n):
for i in range(n):
yield i ** 2
# Usage
squares = squares_generator(5)
for square in squares:
print(square)
A versão do gerador é mais curta, mais fácil de ler e não requer código padrão. Esse é um exemplo perfeito da filosofia do Python: simples é melhor.
Manuseio de sequências infinitas
Por fim, gostaria de dizer que os geradores são especialmente adequados para representar sequências infinitas, algo que é simplesmente impossível com listas. Por exemplo, considere a sequência de Fibonacci:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Usage
fib = fibonacci()
for _ in range(10):
print(next(fib))
0
1
1
2
3
5
8
13
21
34
Esse gerador pode produzir números de Fibonacci indefinidamente sem ficar sem memória. Outros exemplos incluem o processamento de fluxos de dados ao vivo ou o trabalho com dados de séries temporais.
Conceitos avançados do gerador Python
Agora, vamos dar uma olhada em algumas ideias mais difíceis. Nesta seção, exploraremos como compor geradores e usar métodos exclusivos de geradores, como .send()
, .throw()
e .close()
.
Encadeamento de geradores
Os geradores podem ser combinados. Você pode transformar, filtrar e processar dados de forma modular encadeando geradores.
Digamos que você tenha uma sequência infinita de números e queira elevar cada número ao quadrado e filtrar os resultados ímpares:
def infinite_sequence():
num = 0
while True:
yield num
num += 1
def square_numbers(sequence):
for num in sequence:
yield num ** 2
def filter_evens(sequence):
for num in sequence:
if num % 2 == 0:
yield num
# Compose the generators
numbers = infinite_sequence()
squared = square_numbers(numbers)
evens = filter_evens(squared)
# Print the first 10 even squares
for _ in range(10):
print(next(evens))
0
4
16
36
64
100
144
196
256
324
O processo envolve a função infinite_sequence
que gera números indefinidamente, enquanto a square_numbers
produz o quadrado de cada número e, em seguida, a filter_evens
filtra os números ímpares para produzir apenas quadrados pares. Nosso plano de carreira Associate Python Developer aborda esse tipo de coisa, para que você possa ver como criar e depurar pipelines complexos usando geradores, bem como iteradores e compreensões de lista.
Métodos especiais de gerador
Os geradores vêm com métodos avançados que permitem comunicação bidirecional e terminação controlada.
send()
O método .send()
permite que você passe valores de volta para um gerador, transformando-o em uma corrotina. Isso é útil para criar geradores interativos ou com estado.
def accumulator():
total = 0
while True:
value = yield total
if value is not None:
total += value
# Using the generator
acc = accumulator()
next(acc) # Start the generator
print(acc.send(10)) # Output: 10
print(acc.send(5)) # Output: 15
print(acc.send(20)) # Output: 35
Veja como isso funciona:
-
O gerador começa com
next(acc)
para ser inicializado. -
Cada chamada para
.send(value)
passa um valor para o gerador, que é atribuído avalue
na instruçãoyield
. -
O gerador atualiza seu estado (
total
) e produz o novo resultado.
throw()
O método .throw()
permite que você crie uma exceção dentro do gerador, o que pode ser útil para o tratamento de erros ou para sinalizar condições específicas.
def resilient_generator():
try:
for i in range(5):
yield i
except ValueError:
yield "Error occurred!"
# Using the generator
gen = resilient_generator()
print(next(gen)) # Output: 0
print(next(gen)) # Output: 1
print(gen.throw(ValueError)) # Output: "Error occurred!"
Veja como isso funciona:
-
Em geral, o gerador funciona até que o
.throw()
seja chamado. -
A exceção é levantada dentro do gerador, que pode tratá-la usando um bloco
try-except
.
close()
O método .close()
interrompe um gerador levantando uma exceção GeneratorExit
. Isso é útil para limpar recursos ou interromper geradores infinitos.
def infinite_counter():
count = 0
try:
while True:
yield count
count += 1
except GeneratorExit:
print("Generator closed!")
# Using the generator
counter = infinite_counter()
print(next(counter)) # Output: 0
print(next(counter)) # Output: 1
counter.close() # Output: "Generator closed!"
E é assim que está funcionando:
-
O gerador é executado até que o endereço
.close()
seja chamado. -
A exceção
GeneratorExit
é levantada, permitindo que o gerador limpe ou registre uma mensagem antes de encerrar.
Aplicativos do mundo real em ciência de dados
Espero que você esteja começando a perceber que os geradores são úteis. Nesta seção, tentarei destacar os casos de uso para que você possa imaginar como eles realmente funcionam para você no seu dia a dia.
Processamento de grandes conjuntos de dados
Um dos desafios mais comuns na ciência de dados é trabalhar com conjuntos de dados grandes demais para caber na memória. Os geradores oferecem uma maneira de processar esses dados linha por linha.
Imagine que você tenha um arquivo CSV de 10 GB contendo dados de vendas e precise filtrar os registros de uma região específica. Veja como você pode usar um pipeline de gerador para conseguir isso:
import csv
def read_large_csv(file_path):
""" Generator to read a large CSV file line by line."""
with open(file_path, mode="r") as file:
reader = csv.DictReader(file)
for row in reader:
yield row
def filter_by_region(data, region):
""" Generator to filter rows by a specific region."""
for row in data:
if row["Region"] == region:
yield row
# Generator pipeline
file_path = "sales_data.csv"
region = "North America"
data = read_large_csv(file_path)
filtered_data = filter_by_region(data, region)
# Process the filtered data
for record in filtered_data:
print(record)
O que está acontecendo é o seguinte:
-
read_large_csv
lê o arquivo linha por linha, produzindo cada linha como um dicionário. -
filter_by_region
filtra as linhas com base na região especificada. -
O pipeline processa os dados de forma incremental, evitando a sobrecarga de memória.
Essa abordagem beneficia os fluxos de trabalho de extração, transformação e carregamento, nos quais os dados devem ser limpos e transformados antes da análise. Você verá esse tipo de coisa em nosso curso ETL e ELT em Python.
Streaming e pipelines
Às vezes, os dados chegam como um fluxo contínuo. Pense em dados de sensores, feeds ao vivo ou mídia social.
Suponha que você esteja trabalhando com dispositivos IoT que geram leituras de temperatura a cada segundo. Você deseja calcular a temperatura média em uma janela deslizante de 10 leituras:
def sensor_data_stream():
"""Simulate an infinite stream of sensor data."""
import random
while True:
yield random.uniform(0, 100) # Simulate sensor data
def sliding_window_average(stream, window_size):
""" Calculate the average over a sliding window of readings."""
window = []
for value in stream:
window.append(value)
if len(window) > window_size:
window.pop(0)
if len(window) == window_size:
yield sum(window) / window_size
# Generator pipeline
sensor_stream = sensor_data_stream()
averages = sliding_window_average(sensor_stream, window_size=10)
# Print the average every second
for avg in averages:
print(f"Average temperature: {avg:.2f}")
Aqui está a explicação:
-
sensor_data_stream
simula um fluxo infinito de leituras de sensores. -
sliding_window_average
mantém uma janela deslizante das últimas 10 leituras e produz sua média. -
O pipeline processa dados em tempo real, o que o torna ideal para monitoramento e análise.
Casos de uso adicionais
Os geradores também são usados em situações em que o tamanho dos dados é imprevisível ou quando eles não param de chegar ou são infinitos.
Raspagem da Web
Ao fazer scraping de sites, você geralmente não sabe quantas páginas ou itens precisará processar. Os geradores permitem que você lide com essa imprevisibilidade de forma elegante:
def scrape_website(url):
""" Generator to scrape a website page by page."""
while url:
# Simulate fetching and parsing a page
print(f"Scraping {url}")
data = f"Data from {url}"
yield data
url = get_next_page(url) # Hypothetical function to get the next page
# Usage
scraper = scrape_website("https://example.com/page1")
for data in scraper:
print(data)
Tarefas de simulação
Em simulações, como métodos de Monte Carlo ou desenvolvimento de jogos, os geradores podem representar sequências infinitas ou dinâmicas:
def monte_carlo_simulation():
""" Generator to simulate random events for Monte Carlo analysis."""
import random
while True:
yield random.random()
# Usage
simulation = monte_carlo_simulation()
for _ in range(10):
print(next(simulation))
Benchmarks de memória e velocidade
Devido ao modo como funcionam, os geradores são excelentes em cenários em que a eficiência da memória é fundamental, mas (você pode se surpreender ao saber) eles nem sempre são a opção mais rápida. Vamos comparar os geradores com as listas para entender suas vantagens e desvantagens.
Anteriormente, mostramos como os geradores eram melhores do que as listas em termos de memória. Essa foi a parte em que comparamos o uso da memória ao gerar uma sequência de 10 milhões de números. Agora vamos fazer uma comparação de velocidade diferente:
import time
# List comprehension
start_time = time.time()
sum([x**2 for x in range(1_000_000)])
print(f"List comprehension time: {time.time() - start_time:.4f} seconds")
# Generator expression
start_time = time.time()
sum(x**2 for x in range(1_000_000))
print(f"Generator expression time: {time.time() - start_time:.4f} seconds")
List comprehension time: 0.1234 seconds
Generator expression time: 0.1456 seconds
Embora um gerador economize memória, nesse caso, ele é realmente mais lento do que a lista. Isso ocorre porque, para esse conjunto de dados menor, há a sobrecarga de pausar e retomar a execução.
A diferença de desempenho é insignificante para conjuntos de dados pequenos, mas para conjuntos de dados grandes, a economia de memória dos geradores geralmente supera a pequena penalidade de velocidade.
Problemas que surgem
Por fim, vamos examinar alguns erros ou problemas comuns:
Os geradores são esgotáveis
Quando um gerador se esgota, ele não pode ser reutilizado. Você precisará recriá-lo se quiser iterar novamente.
gen = (x for x in range(5))
print(list(gen)) # Output: [0, 1, 2, 3, 4]
print(list(gen)) # Output: [] (the generator is exhausted)
A avaliação preguiçosa pode ser complicada
Como os geradores produzem valores sob demanda, os erros ou efeitos colaterais podem não aparecer até que o gerador seja iterado.
Você pode usar geradores em excesso
Para conjuntos de dados pequenos ou tarefas simples, a sobrecarga de usar um gerador pode não valer a economia de memória. Considere este exemplo em que estou materializando dados para várias iterações.
# Generator expression
gen = (x**2 for x in range(10))
# Materialize into a list
squares = list(gen)
# Reuse the list
print(sum(squares)) # Output: 285
print(max(squares)) # Output: 81
Escolhendo quando usar geradores
Para recapitular, fornecerei algumas regras muito gerais sobre quando usar geradores. Use para:
- Grandes conjuntos de dados: Use geradores ao trabalhar com conjuntos de dados muito grandes para caber na memória.
- Sequências infinitas: Use geradores para representar sequências infinitas, como fluxos de dados ao vivo ou simulações.
- Pipelines: Use geradores para criar pipelines de processamento de dados modulares que transformam e filtram dados de forma incremental.
Quando, em vez disso, você deve materializar os dados (converter em uma lista)
- Conjuntos de dados pequenos: Não use geradores se a memória não for um problema e você precisar de acesso rápido a todos os elementos; em vez disso, use uma lista.
- Múltiplas iterações: Não use geradores se você precisar iterar sobre os mesmos dados várias vezes; em vez disso, materialize-os em uma lista para evitar recriar o gerador.
Conclusão e principais conclusões
Ao longo deste artigo, exploramos como os geradores podem ajudar você a enfrentar os desafios do mundo real na ciência de dados, desde o processamento de grandes conjuntos de dados até a criação de pipelines de dados em tempo real. Continue praticando. A melhor maneira de dominar os geradores é usá-los em seu próprio trabalho. Para começar, tente substituir uma compreensão de lista por uma expressão geradora ou refatorar um loop em uma função geradora.
Depois de dominar os conceitos básicos, você poderá explorar tópicos novos e mais avançados que se baseiam no conceito de gerador:
-
Rotinas: Use
.send()
e.throw()
para criar geradores que possam receber e processar dados, permitindo a comunicação bidirecional. -
Programação assíncrona: Combine geradores com a biblioteca asyncio do Python para criar aplicativos eficientes e sem bloqueio.
-
Concorrência: Saiba como os geradores podem implementar a multitarefa cooperativa e a concorrência leve.
Continue aprendendo e torne-se um especialista. Faça hoje mesmo nosso curso de carreira de Desenvolvedor Python ou nosso curso de habilidades de Programação Python. Clique no link abaixo para começar.
Torne-se um cientista de ML
Escritor técnico especializado em IA, ML e ciência de dados, tornando ideias complexas claras e acessíveis.
Perguntas frequentes sobre geradores Python
O que exatamente é um gerador Python?
Um gerador Python é um tipo especial de função que usa a função yield para retornar um iterador, produzindo valores um de cada vez e conservando a memória por não armazenar a sequência inteira de uma vez.
Quando devo usar geradores em vez de compreensões de lista?
Os geradores são ideais para processar sequências grandes ou infinitas quando a eficiência da memória é fundamental, enquanto as compreensões de lista funcionam bem quando você precisa de uma lista completa para acesso repetido ou indexação aleatória.
Como os geradores aumentam o desempenho?
Ao fornecer um valor de cada vez (avaliação preguiçosa), os geradores calculam valores em tempo real, o que reduz o uso da memória e acelera o processamento em comparação com a criação de estruturas de dados completas na memória.
Posso iterar sobre um gerador mais de uma vez?
Não, os geradores se esgotam após uma iteração completa. Para iterar novamente, você precisa criar uma nova instância do gerador.
Quais são alguns casos de uso prático para geradores?
Os geradores são úteis para processar grandes conjuntos de dados, transmitir dados em tempo real, criar pipelines eficientes e lidar com sequências infinitas, como as encontradas em tarefas algorítmicas e de simulação.
Aprenda Python com o DataCamp
curso
Intermediate Python
curso
Introduction to Python for Developers

blog
5 desafios Python para desenvolver suas habilidades

DataCamp Team
5 min

blog
6 práticas recomendadas de Python para um código melhor
tutorial
Tutorial de iteradores e geradores Python
tutorial
21 ferramentas essenciais do Python
tutorial
Otimização em Python: Técnicas, pacotes e práticas recomendadas
tutorial