Aplicações do Mundo Real de Iteradores
Agora que você entende como criar e usar iteradores, vamos explorar algumas aplicações práticas do mundo real onde os iteradores podem melhorar seu código.
Avaliação Preguiçosa com Geradores
Geradores são um tipo especial de iterador criado com funções que usam a instrução yield. Eles permitem que você gere valores sob demanda, o que pode ser mais eficiente em termos de memória do que criar uma lista completa.
Crie um arquivo chamado generator_example.py:
def squared_numbers(n):
"""Generate squares of numbers from 1 to n."""
for i in range(1, n + 1):
yield i * i
## Create a generator for squares of numbers 1 to 10
squares = squared_numbers(10)
## squares is a generator object (a type of iterator)
print(f"Type of squares: {type(squares)}")
## Use next() to get values from the generator
print("\nGetting values with next():")
print(next(squares)) ## 1
print(next(squares)) ## 4
print(next(squares)) ## 9
## Use a for loop to get the remaining values
print("\nGetting remaining values with a for loop:")
for square in squares:
print(square)
## The generator is now exhausted, so this won't print anything
print("\nTrying to get more values (generator is exhausted):")
for square in squares:
print(square)
## Create a new generator and convert all values to a list at once
all_squares = list(squared_numbers(10))
print(f"\nAll squares as a list: {all_squares}")
Execute o código:
python3 ~/project/generator_example.py
Você verá:
Type of squares: <class 'generator'>
Getting values with next():
1
4
9
Getting remaining values with a for loop:
16
25
36
49
64
81
100
Trying to get more values (generator is exhausted):
All squares as a list: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Geradores são uma maneira mais concisa de criar iteradores para casos simples. Eles implementam automaticamente o protocolo do iterador para você.
Processamento de Grandes Conjuntos de Dados
Iteradores são perfeitos para processar grandes conjuntos de dados porque permitem que você trabalhe com um elemento de cada vez. Vamos criar um exemplo que simula o processamento de um grande conjunto de dados de temperaturas:
Crie um arquivo chamado data_processing.py:
import random
import time
def temperature_data_generator(days, start_temp=15.0, max_variation=5.0):
"""Generate simulated hourly temperature data for a number of days."""
hours_per_day = 24
total_hours = days * hours_per_day
current_temp = start_temp
for hour in range(total_hours):
## Simulate temperature variations
day_progress = (hour % hours_per_day) / hours_per_day ## 0.0 to 1.0 through the day
## Temperature is generally cooler at night, warmer during day
time_factor = -max_variation/2 * (
-2 * day_progress + 1 if day_progress < 0.5
else 2 * day_progress - 1
)
## Add some randomness
random_factor = random.uniform(-1.0, 1.0)
current_temp += time_factor + random_factor
current_temp = max(0, min(40, current_temp)) ## Keep between 0-40°C
yield (hour // hours_per_day, hour % hours_per_day, round(current_temp, 1))
def process_temperature_data():
"""Process a large set of temperature data using an iterator."""
print("Processing hourly temperature data for 30 days...")
print("-" * 50)
## Create our data generator
data_iterator = temperature_data_generator(days=30)
## Track some statistics
total_readings = 0
temp_sum = 0
min_temp = float('inf')
max_temp = float('-inf')
## Process the data one reading at a time
start_time = time.time()
for day, hour, temp in data_iterator:
## Update statistics
total_readings += 1
temp_sum += temp
min_temp = min(min_temp, temp)
max_temp = max(max_temp, temp)
## Just for demonstration, print a reading every 24 hours
if hour == 12: ## Noon each day
print(f"Day {day+1}, 12:00 PM: {temp}°C")
processing_time = time.time() - start_time
## Calculate final statistics
avg_temp = temp_sum / total_readings if total_readings > 0 else 0
print("-" * 50)
print(f"Processed {total_readings} temperature readings in {processing_time:.3f} seconds")
print(f"Average temperature: {avg_temp:.1f}°C")
print(f"Temperature range: {min_temp:.1f}°C to {max_temp:.1f}°C")
## Run the temperature data processing
process_temperature_data()
Execute o código:
python3 ~/project/data_processing.py
Você verá uma saída semelhante a (as temperaturas exatas variarão devido à aleatoriedade):
Processing hourly temperature data for 30 days...
--------------------------------------------------
Day 1, 12:00 PM: 17.5°C
Day 2, 12:00 PM: 18.1°C
Day 3, 12:00 PM: 17.3°C
...
Day 30, 12:00 PM: 19.7°C
--------------------------------------------------
Processed 720 temperature readings in 0.012 seconds
Average temperature: 18.2°C
Temperature range: 12.3°C to 24.7°C
Neste exemplo, estamos usando um iterador para processar um conjunto de dados simulado de 720 leituras de temperatura (24 horas × 30 dias) sem ter que armazenar todos os dados na memória de uma vez. O iterador gera cada leitura sob demanda, tornando o código mais eficiente em termos de memória.
Construindo um Pipeline de Dados com Iteradores
Iteradores podem ser encadeados para criar pipelines de processamento de dados. Vamos construir um pipeline simples que:
- Gera números
- Filtra números ímpares
- Eleva ao quadrado os números pares restantes
- Limita a saída a um número específico de resultados
Crie um arquivo chamado data_pipeline.py:
def generate_numbers(start, end):
"""Generate numbers in the given range."""
print(f"Starting generator from {start} to {end}")
for i in range(start, end + 1):
print(f"Generating: {i}")
yield i
def filter_even(numbers):
"""Filter for even numbers only."""
for num in numbers:
if num % 2 == 0:
print(f"Filtering: {num} is even")
yield num
else:
print(f"Filtering: {num} is odd (skipped)")
def square_numbers(numbers):
"""Square each number."""
for num in numbers:
squared = num ** 2
print(f"Squaring: {num} → {squared}")
yield squared
def limit_results(iterable, max_results):
"""Limit the number of results."""
count = 0
for item in iterable:
if count < max_results:
print(f"Limiting: keeping item #{count+1}")
yield item
count += 1
else:
print(f"Limiting: reached maximum of {max_results} items")
break
## Create our data pipeline
print("Creating data pipeline...\n")
pipeline = (
limit_results(
square_numbers(
filter_even(
generate_numbers(1, 10)
)
),
3 ## Limit to 3 results
)
)
## Execute the pipeline by iterating through it
print("\nExecuting pipeline and collecting results:")
print("-" * 50)
results = list(pipeline)
print("-" * 50)
print(f"\nFinal results: {results}")
Execute o código:
python3 ~/project/data_pipeline.py
Você verá:
Creating data pipeline...
Executing pipeline and collecting results:
--------------------------------------------------
Starting generator from 1 to 10
Generating: 1
Filtering: 1 is odd (skipped)
Generating: 2
Filtering: 2 is even
Squaring: 2 → 4
Limiting: keeping item #1
Generating: 3
Filtering: 3 is odd (skipped)
Generating: 4
Filtering: 4 is even
Squaring: 4 → 16
Limiting: keeping item #2
Generating: 5
Filtering: 5 is odd (skipped)
Generating: 6
Filtering: 6 is even
Squaring: 6 → 36
Limiting: keeping item #3
Generating: 7
Filtering: 7 is odd (skipped)
Generating: 8
Filtering: 8 is even
Squaring: 8 → 64
Limiting: reached maximum of 3 items
--------------------------------------------------
Final results: [4, 16, 36]
Este exemplo de pipeline mostra como os iteradores podem ser conectados para formar um fluxo de trabalho de processamento de dados. Cada estágio do pipeline processa um item de cada vez, passando-o para o próximo estágio. O pipeline não processa nenhum dado até que realmente consumamos os resultados (neste caso, convertendo-o em uma lista).
A principal vantagem é que nenhuma lista intermediária é criada entre os estágios do pipeline, tornando essa abordagem eficiente em termos de memória, mesmo para grandes conjuntos de dados.