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 faz seu computador travar. Ou você está lidando com uma sequência infinita, como um fluxo de dados ao vivo, onde você não pode armazenar tudo ao mesmo tempo. Esses são os tipos de desafios que fazem os cientistas de dados recorrerem à cafeteira—e às vezes, ao botão de reset.
Neste artigo, aprenderemos sobre geradores em Python e como você pode usá-los para simplificar seu código. Essa ideia requer prática, então, se você é novo em Python e se perder um pouco neste artigo, experimente nosso Curso de Introdução ao Python para construir uma base sólida.
O que são geradores em Python?
No seu núcleo, os geradores do 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 esteira em uma fábrica: em vez de empilhar todos os produtos em um lugar e ficar sem espaço, você processa cada item conforme ele vem pela 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 e compreensões.
A magia por trás dos geradores está na palavra-chave yield
. Ao contrário de return
, que produz um único valor e sai da função, yield
produz um valor, pausa a execução da função e salva seu estado. Quando o gerador é chamado novamente, ele retoma de onde parou.
Por exemplo, imagine que você está lendo um arquivo de log enorme linha por linha. Um gerador pode processar cada linha conforme lida, sem carregar o arquivo inteiro na memória. Essa “avaliação preguiçosa” distingue os geradores das funções tradicionais e os torna uma ferramenta indispensável para tarefas sensíveis ao desempenho.
Um Exemplo Básico de Gerador Python
Vamos praticar um pouco para entender a ideia. Aqui está uma função geradora que produz os primeiros n
inteiros.
def generate_integers(n): for i in range(n): yield i # Pausa aqui e retorna i # Usando a geradora for num in generate_integers(5): print(num)
0 1 2 3 4
Eu criei uma visualização para ajudar você a ver o que está acontecendo por baixo dos panos:
Python Sintaxe e Padrões de Geradores
Geradores podem ser implementados de várias maneiras. Dito isso, existem duas maneiras principais: funções geradoras e expressões geradoras.
Funções geradoras
Uma função geradora é definida como uma função comum, 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 # Usando a geradora counter = count_up_to(5) for num in counter: print(num)
1 2 3 4 5
Do 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 geradoras
Expressões geradoras são uma maneira compacta de criar geradores. Elas são semelhantes às compreensões de lista, mas com parênteses em vez de colchetes.
# Compreensão de lista (avaliação ansiosa) squares_list = [x**2 for x in range(5)] # [0, 1, 4, 9, 16] # Expressão geradora (avaliação preguiçosa) squares_gen = (x**2 for x in range(5)) # Usando o gerador 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 geradora? A compreensão de lista cria toda a lista na memória, enquanto a expressão geradora produz valores um a um, economizando memória. Se você não está familiarizado com compreensões de lista, pode ler sobre elas em nosso Tutorial de Compreensão de Lista em Python.
Gerador Python vs. Iterador
Iteradores tradicionais em Python exigiam classes com métodos explícitos __iter__()
e __next__()
, o que envolvia muito código repetitivo e gerenciamento manual de estado, enquanto as funções geradoras simplificam o processo ao preservar automaticamente o estado e eliminar a necessidade desses métodos—como demonstrado por uma função simples que gera o quadrado de cada número até n
.
Por que usamos geradores Python
Ao explicar o que são geradores em Python, também transmiti um pouco da ideia de por que eles são usados. Nesta seção, quero entrar em um pouco mais de detalhe. Porque os geradores não são apenas um recurso sofisticado do Python, mas realmente resolvem problemas reais.
Eficiência de memória
Diferentemente de listas ou arrays, que armazenam todos os seus elementos na memória simultaneamente, os geradores produzem valores instantaneamente, portanto, mantêm apenas um item na memória por vez.
Por exemplo, considere a diferença entre range()
e xrange()
do Python 2:
-
range()
criava uma lista na memória, o que poderia ser problemático para intervalos grandes. -
xrange()
agia como um gerador, produzindo valores de forma preguiçosa.
Como o comportamento do xrange()
era mais útil, agora, no Python 3, range()
também se comporta como um gerador, evitando a sobrecarga de memória de armazenar todos os valores simultaneamente.
Para mostrar a ideia, vamos comparar o uso de memória ao gerar uma sequência de 10 milhões de números:
import sys # Usando uma lista 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") # Usando um gerador 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 usa quase nenhuma memória em comparação com a lista, e essa diferença é significativa.
Melhorias de desempenho
Graças à avaliação preguiçosa, os valores são calculados apenas quando necessário. Isso significa que você pode começar a processar 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:
# Usando uma lista (avaliação ansiosa) sum_of_squares_list = sum([x**2 for x in range(1_000_000)]) # Usando um gerador (avaliação preguiçosa) sum_of_squares_gen = sum(x**2 for x in range(1_000_000))
Embora ambas as abordagens deem o mesmo resultado, a versão do gerador evita a criação de uma lista massiva, então obtemos o resultado mais rápido.
simplicidade e legibilidade
Geradores simplificam a implementação de iteradores ao eliminar código repetitivo. 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 # Uso squares = SquaresIterator(5) for square in squares: print(square)
Aqui está a função geradora:
def squares_generator(n): for i in range(n): yield i ** 2 # Uso squares = squares_generator(5) for square in squares: print(square)
A versão geradora é mais curta, mais fácil de ler e não requer código repetitivo. É um exemplo perfeito da filosofia do Python: simples é melhor.
Tratando sequências infinitas
Finalmente, quero 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 # Uso fib = fibonacci() for _ in range(10): print(next(fib))
0 1 1 2 3 5 8 13 21 34
Este 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 de Geradores em Python
Agora, vamos explorar algumas ideias mais difíceis. Nesta seção, vamos investigar como compor geradores e usar métodos de gerador exclusivos como .send()
, .throw()
e .close()
.
Encadeando geradores juntos
Os geradores podem ser combinados. Você pode transformar, filtrar e processar dados de forma modular encadeando geradores.
Vamos supor que você tenha uma sequência infinita de números e queira quadrar cada número 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 # Compor os geradores numbers = infinite_sequence() squared = square_numbers(numbers) evens = filter_evens(squared) # Imprimir os primeiros 10 quadrados pares 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
gerando números indefinidamente, enquanto square_numbers
fornece o quadrado de cada número, e então filter_evens
filtra os números ímpares para produzir apenas quadrados pares. Nossa trilha de carreira como Desenvolvedor Python Associado aborda esse tipo de coisa, para que você possa ver como construir 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 # Usando o gerador acc = accumulator() next(acc) # Inicie o gerador print(acc.send(10)) # Saída: 10 print(acc.send(5)) # Saída: 15 print(acc.send(20)) # Saída: 35
Aqui está como funciona:
-
O gerador começa com
next(acc)
para inicializá-lo. -
Cada chamada para
.send(value)
passa um valor para o gerador, que é atribuído avalue
na declaraçãoyield
. -
O gerador atualiza seu estado (
total
) e retorna o novo resultado.
throw()
O método .throw()
permite que você levante uma exceção dentro do gerador, o que pode ser útil para tratamento de erros ou sinalização de condições específicas.
def resilient_generator(): try: for i in range(5): yield i except ValueError: yield "Error occurred!" # Usando o gerador gen = resilient_generator() print(next(gen)) # Saída: 0 print(next(gen)) # Saída: 1 print(gen.throw(ValueError)) # Saída: "Ocorreu um erro!"
Veja como isso funciona:
-
O gerador geralmente funciona até que
.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 parar geradores infinitos.
def infinite_counter(): count = 0 try: while True: yield count count += 1 except GeneratorExit: print("Generator closed!") # Usando o gerador counter = infinite_counter() print(next(counter)) # Saída: 0 print(next(counter)) # Saída: 1 counter.close() # Saída: "Gerador fechado!"
E aqui está como isso funciona:
-
O gerador executa até que
.close()
seja chamado. -
A exceção
GeneratorExit
é levantada, permitindo que o gerador faça a limpeza ou registre uma mensagem antes de terminar.
Aplicações 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, vou tentar 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 em ciência de dados é trabalhar com conjuntos de dados grandes demais para caber na memória. Os geradores fornecem uma maneira de processar esses dados linha por linha.
Imagine que você tem um arquivo CSV de 10 GB contendo dados de vendas e precisa filtrar registros para uma região específica. Veja como você pode usar um pipeline de gerador para alcançar 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 # Pipeline de gerador file_path = "sales_data.csv" region = "North America" data = read_large_csv(file_path) filtered_data = filter_by_region(data, region) # Processar os dados filtrados for record in filtered_data: print(record)
Aqui está o que está acontecendo:
-
read_large_csv
lê o arquivo linha por linha, retornando cada linha como um dicionário. -
filter_by_region
filtra linhas com base na região especificada. -
O pipeline processa dados de forma incremental, evitando sobrecarga de memória.
Essa abordagem beneficia fluxos de trabalho de extração, transformação e carregamento, onde os dados devem ser limpos e transformados antes da análise. Você verá esse tipo de coisa em nosso curso de ETL e ELT em Python.
Streaming e pipelines
Os dados às vezes chegam como um fluxo contínuo. Pense em dados de sensores, feeds ao vivo ou redes sociais.
Suponha que você esteja trabalhando com dispositivos de IoT que geram leituras de temperatura a cada segundo. Você quer 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) # Simular dados do sensor 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 # Pipeline do gerador sensor_stream = sensor_data_stream() averages = sliding_window_average(sensor_stream, window_size=10) # Imprimir a média a cada segundo 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 fornece sua média. -
O pipeline processa dados em tempo real, tornando-o ideal para monitoramento e análises.
Casos de uso adicionais
Geradores também são usados em situações onde o tamanho dos dados é imprevisível ou quando eles continuam chegando/é infinito.
Raspagem da web
Ao raspar sites, você muitas vezes não sabe quantas páginas ou itens precisará processar. 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: # Simular a obtenção e análise de uma página print(f"Scraping {url}") data = f"Data from {url}" yield data url = get_next_page(url) # Função hipotética para obter a próxima página # Uso 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, 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() # Uso simulation = monte_carlo_simulation() for _ in range(10): print(next(simulation))
Benchmarks de Memória e Velocidade
Por causa de como funcionam, geradores se destacam em cenários onde a eficiência de memória é crítica, mas (você pode se surpreender ao saber) eles podem não ser sempre a opção mais rápida. Vamos comparar geradores com listas para entender suas compensações.
Anteriormente, mostramos como os geradores eram melhores do que listas em termos de memória. Esta foi a parte onde comparamos o uso de memória ao gerar uma sequência de 10 milhões de números. Agora, vamos fazer algo diferente, uma comparação de velocidade:
import time # Compreensão de lista 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") # Expressão de gerador 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
Enquanto um gerador economiza memória, neste caso, ele é na verdade mais lento que a lista. Isso ocorre porque, para este conjunto de dados menor, há a sobrecarga de pausar e retomar a execução.
A diferença de desempenho é negligenciável para conjuntos de dados pequenos, mas para conjuntos de dados grandes, as economias de memória dos geradores frequentemente superam a ligeira penalização de velocidade.
Problemas que Surgem
Finalmente, vamos olhar para alguns erros ou problemas comuns:
Geradores são exauríveis
Uma vez que um gerador está esgotado, ele não pode ser reutilizado. Você precisará recriá-lo se quiser iterar novamente.
gen = (x for x in range(5)) print(list(gen)) # Saída: [0, 1, 2, 3, 4] print(list(gen)) # Saída: [] (o gerador está esgotado)
A avaliação preguiçosa pode ser complicada
Como os geradores produzem valores sob demanda, 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, o custo de usar um gerador pode não valer a economia de memória. Considere este exemplo em que estou materializando dados para múltiplas iterações.
# Expressão de gerador gen = (x**2 for x in range(10)) # Materializar em uma lista squares = list(gen) # Reutilizar a lista print(sum(squares)) # Saída: 285 print(max(squares)) # Saída: 81
Escolhendo Quando Usar Geradores
Para recapitular, vou fornecer algumas regras muito gerais sobre quando usar geradores. Use para:
- Conjuntos de dados grandes: Use geradores ao trabalhar com conjuntos de dados grandes demais 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 construir pipelines de processamento de dados modulares que transformam e filtram dados de forma incremental.
Quando materializar dados em vez disso (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 Pontos
Ao longo deste artigo, exploramos como geradores podem ajudá-lo a enfrentar desafios do mundo real em ciência de dados, desde o processamento de grandes conjuntos de dados até a construção de pipelines de dados em tempo real. Continue praticando. A melhor maneira de dominar geradores é usá-los em seu próprio trabalho. Como um começo, tente substituir uma compreensão de lista por uma expressão de gerador ou refatorar um loop em uma função geradora.
Uma vez que você tenha dominado o básico, você pode explorar novos e mais avançados tópicos que se baseiam no conceito de gerador:
-
Corrotinas: Use
.send()
e.throw()
para criar geradores que podem receber e processar dados, possibilitando comunicação bidirecional. -
Programação assíncrona: Combine geradores com a biblioteca asyncio do Python para construir aplicações eficientes e não bloqueantes.
-
Concorrência: Aprenda como os geradores podem implementar multitarefa cooperativa e concorrência leve.
Continue aprendendo e torne-se um especialista. Faça nosso percurso de carreira como Desenvolvedor Python ou nossa trilha de habilidades em Programação Python hoje. Clique no link abaixo para começar.