Article image
Antonio Guedes
Antonio Guedes03/07/2025 20:55
Compartilhe

Iteradores - Dão banho no fluxo de dados

  • #Python

Entender os iteradores não é tão difícil, quando se entende o conceito de `fluxo de dados`.

Fluxo de Dados (data stream) é uma corrente de dados que pode ser infinita ou extremamente grande. Há um ditado que diz: "No rio a mesma água não passa duas vezes". Assim também é com o fluxo de dados. Os dados vem e vão e não voltam, segue o fluxo.

Imagina que você está assistindo a uma partida de futebol no estádio, será que é possível haver um replay? Não, lá no estádio os lances seguem um fluxo contínuo e não voltam. Assim é o fluxo de dados.

  • Uma sequência continua e potencialmente infinita de dados;
  • Os dados chegam (ou são gerados) incrementalmente ao longo do tempo;
  • Você processa esses dados à medida que eles chegam, em vez de esperar que todos os dados estejam disponíveis para começar;
  • Pense em cada momento de sua vida como um item de dado que passa pelo sistema.

Para ficar mais fácil de entender, vamos comparar fluxo de dados com algo que conhecemos bem desde que somos apresentados ao mundo da computação: Arquivos

ARQUIVOS:

  • Os arquivos são Dados Estáticos e Finitos, que existem em um determinado momento, seja no disco ou na memória;
  • Normalmente acessamos o arquivo todo ou partes dele, mas o arquivo tem começo e fim;

Exemplo: Arquivos .txt, uma imagem .jpg, um banco de dados salvo, uma lista Python completa na memória.

FLUXO DE DADOS:

  • Uma sequência contínua, que pode ser infinita ou muito grande para caber na memória de uma vez;
  • Processamos os dados sequencialmente, conforme eles passam. Um vez que um item de dados é processado, geralmente ele "passa" e não pode ser facilmente revisitado;

Exemplo:

  • Áudio/Vídeo em tempo real;
  • Dados de sensores;
  • Logs de servidor - as linhas de log são geradas a todo momento;
  • Mensagens em redes sociais: tweets e posts publicados a cada segundo.

Por que é crucial o conceito de Fluxo de Dados?

Eficiência de memória: Não há a necessidade de carregar todos os dados na memória de uma vez. Evita travamentos por falta de memória.

Processamento em Tempo Real: Sistemas que precisam reagir a eventos á medida que eles acontecem (detecção de fraude em transações financeiras, monitoramento de saúde).

Latência Reduzida: Por ser processado em tempo real, o tempo entre a geração do dado e seu processamento é minimizado.

Escalabilidade: Sistema de processamento de fluxo são projetados para escalar horizontalmente, distribuindo a carga de trabalho à medida que o volume de dados aumenta.

Como Python Lida com Fluxo de Dados: Iteradores e Geradores.

Neste post eu falarei de iteradores, e deixarei os geradores para outro post.

O que são iteradores? São classes que implementam os métodos especiais __iter__ e __next__. Caso você tenha um objeto e fizer o comando

print(dir(objeto))

E na lista que é apresentada você encontrar os métodos: __iter__ e __next__ você está trabalhando com um objeto iterador. O Método __next__() serve para obter o próximo item e o fim dos dados disponíveis é sinalizado pela exceção StopIteration.

Caso você tenha uma lista em python na memória e você passar essa lista como argumento para o método iter() e atribuir o objeto gerado a uma variável, você terá um iterador.

numeros = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(type(numeros)) # <class 'list'>

iter_numeros = iter(numeros)
print(type(iter_numeros)) # <class 'list_iterator'>

Vamos a um exemplo prático de iteradores?

Criaremos uma classe que irá produzir a sequência de Fibonacci até um certo limite que será dado pelo usuário.

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 

Aí você deve estar pensando: Mas seria tão mais simples criar uma função que gere esta sequencia, e eu não posso discordar de você entretanto, será que no que diz respeito ao consumo de memória, é o mais eficiente?

# função que gera a sequência de fibonacci
def gerar_fibonacci(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

Para chegarmos a esta definição vamos usar uma lib que calcula o tamanho dos objetos em python: "pympler". É necessário instalá-la para utilizar. Para não poluir o ambiente você pode fazer por meio do ambiente virtual:

python -m venv venv
venv/Scripts/activate # no windows

Agora já no ambiente virtual faça o comando de instalação do pympler com o pip ou outro gerenciador de pacotes que você preferir.

Agora com o pympler instalado vamos fazer um teste:

from pympler.asizeof import asizeof
from time import perf_counter # Vamos comparar também o tempo de execução de ambos

# --- Parâmetros de Teste ---
LIMITE_FIBONACCI = 1000 # usar um limite grande para perceber a diferença de memória
           # Se quiser ver melhor a memória pode aumentar ainda mais

print(f"--- Comparação Fibonacci (limite: {LIMITE_FIBONACCI} números) ---")

### Teste com Iterador (consumo de Memória Baixo e Constante)
print(f"\n@@@ Teste com Iterador @@@")
fib_iterador = FibonacciIterator(LIMITE_FIBONACCI)

inicio_iterador = perf_counter()
memoria_max_iterador = 0
tamanho_iterador = asizeof(fib_iterador)
for i, numero in enumerate(fib_iterador):
if i == 0:
 print(f" Tamanho do objeto iterador (primeira iteração): {tamanho_iterador} bytes")  
 print(f" Tamanho de um numero (ex: {numero}): {asizeof(numero)} bytes")

fim_iterador = perf_counter()
tempo_iterador = fim_iterador - inicio_iterador
print(f" Tempo de execução do iterador: {tempo_iterador:.6f} segundos")
print(f" O iterador não armazena a sequencia completa na memória. Seu consumo é eficiente.")


# TEste da função
print('\n@@@ Teste com função Gera Lista @@@')
inicio_lista = perf_counter()
lista_fibonacci = gerar_fibonacci(LIMITE_FIBONACCI)
fim_lista = perf_counter()
tempo_funcao = fim_lista - inicio_lista

print(f" Tempo de exeucao da função (gerando lista): {tempo_funcao:.6f} segundos")
tamanho_lista = asizeof(lista_fibonacci)
print(f" Tempo de exeucao da função (gerando lista): {tamanho_lista} bytes")

print(f" O 5 primeiros itens da lista: {lista_fibonacci[:5]}")

print('\n Comparando os tempos de execução e consumo de memória')
relacao_tempo = tempo_funcao/tempo_iterador
print(f'\n Tempo funcao/iterador {relacao_tempo:.6f} ')
print(f'\n O tempo da função representa {relacao_tempo:.2%} em relação ao tempo de execução do iterador')

relacao_memoria = tamanho_lista/tamanho_iterador
print(f'\n O consumo de memória da função representa {relacao_memoria:.2%} em relação ao consumo do iterador')

RESULTADOS DOS TESTES:

--- Comparação Fibonacci (limite: 1000 números) ---

@@@ Teste com Iterador @@@

  Tamanho do objeto iterador (primeira iteração): 640 bytes

  Tamanho de um numero (ex: 0): 32 bytes

 Tempo de execução do iterador: 0.001362 segundos

 O iterador não armazena a sequencia completa na memória. Seu consumo é eficiente.

@@@ Teste com função Gera Lista @@@

 Tempo de exeucao da função (gerando lista): 0.000588 segundos

 Tempo de exeucao da função (gerando lista): 83000 bytes

 O 5 primeiros itens da lista: [0, 1, 1, 2, 3]

 Comparando os tempos de execução e consumo de memória

 Tempo funcao/iterador 0.431875

 O tempo da função representa 43.19% em relação ao tempo de execução do iterador

 O consumo de memória da função representa 12968.75% em relação ao consumo do iterador

Você percebe que o iterador realiza uma excelente economia de memória: enquanto gera toda a sequencia de fibonacci de 1000 números e te entrega utilizando apenas 640 byter, a função utiliza 83.000 bytes

De fato, o tempo de execução do iterador é ligeiramente maior que o tempo utilizado pela função, entretanto, quanto maior o fluxo de dados, mais o iterador demonstra seu diferencial.

Mas não são apenas os iteradores que sabem lidar com fluxo de dados em python, os geradores também dão um show, inclusive, com códigos mais pythônicos. Mas isso é assunto para outro post 😉

Compartilhe
Comentários (0)