Iteradores e Geradores em Python: Compreendendo a Iteração Eficiente

Em Python, a capacidade de iterar sobre coleções de dados é fundamental. Dois conceitos-chave que possibilitam e otimizam essa iteração são os iteradores e os geradores. Embora relacionados, eles representam abordagens ligeiramente diferentes para o mesmo objetivo: fornecer uma sequência de valores sob demanda.

Iteradores

Um objeto é considerado um iterador se implemantar o protocolo do iterador, que consiste em dois métodos: __iter__() e __next__().

  • O método __iter__() deve retornar o próprio objeto iterador.
  • O método __next__() deve retornar o próximo item da iteração. Quando não há mais itens, ele deve levantar a exceção StopIteration.

Qualquer objeto que possa ser percorrido (como listas, tuplas, strings, dicionários, etc.) é chamado de iterável. Ao chamar a função embutida iter() em um objeto iterável, obtemos um iterador. Este iterador mantém o controle da posição atual e permite acessar elementos um por um usando a função next().

Utilizando Iteradores

Podemos criar explicitamente um objeto iterador a partir de um iterável e percorrê-lo:


numeros = [10, 20, 30, 40]
it_numeros = iter(numeros) # Cria um objeto iterador

print(next(it_numeros)) # Saída: 10
print(next(it_numeros)) # Saída: 20
print(next(it_numeros)) # Saída: 30

A iteração com um iterador pode ser feita de várias maneiras. A forma mais comum e idiomática é usando um laço for:


cores = ['vermelho', 'verde', 'azul']
iter_cores = iter(cores)

for cor_atual in iter_cores:
    print(cor_atual)
# Saída:
# vermelho
# verde
# azul

Para um controle mais granular ou para entender o mecanismo subjacente do laço for, podemos usar um laço while com tratamento de exceção:


letras = ['a', 'b', 'c', 'd']
iter_letras = iter(letras)

while True:
    try:
        proxima_letra = next(iter_letras)
        print(proxima_letra)
    except StopIteration:
        print("Fim da iteração.")
        break
# Saída:
# a
# b
# c
# d
# Fim da iteração.

Geradores

Em Python, um gerador é uma maneira elegante de criar iteradores. Uma função se torna uma função geradora se contiver a palavra-chave yield. Quando uma função geradora é chamada, ela não executa o código imediatamente nem retorna um valor direto; em vez disso, ela retorna um objeto gerador (que é um tipo de iterador).

A principal característica do yield é que ele pausa a execução da função e retorna um valor. Quando a função é chamada novamente (via next() ou um laço for), ela retoma a execução do ponto onde parou, mantendo seu estado interno.

Isso torna os geradores extremamente eficientes em termos de memória, pois eles produzem valores um de cada vez ("on-the-fly"), em vez de calcular e armazenar todos os valores em memória de uma vez.

Exemplo: Sequência de Fibonacci

Vamos comparar a implementação de uma sequência de Fibonacci tradicional com uma usando um gerador.

Abordagem Tradicional (com impressão direta)


def gerar_fibonacci_imprimir(limite):
    n1, n2 = 0, 1
    cont = 0
    while cont < limite:
        print(n2)
        n1, n2 = n2, n1 + n2
        cont += 1

print("Fibonacci (imprimir direto):")
gerar_fibonacci_imprimir(7)
# Saída:
# Fibonacci (imprimir direto):
# 1
# 1
# 2
# 3
# 5
# 8
# 13

Abordagem com Gerador


def gerar_fibonacci_gerador(limite):
    n1, n2 = 0, 1
    cont = 0
    while cont < limite:
        yield n2 # Pausa aqui e retorna n2
        n1, n2 = n2, n1 + n2
        cont += 1

print("\nFibonacci (via gerador):")
iter_fib = gerar_fibonacci_gerador(7) # Retorna um objeto gerador/iterador

print(next(iter_fib)) # Saída: 1
print(next(iter_fib)) # Saída: 1
print("--- Algum código no meio ---")
print(next(iter_fib)) # Saída: 2
print(next(iter_fib)) # Saída: 3

print("\nContinuando com o loop for:")
for valor_fib in iter_fib:
    print(valor_fib)
# Saída:
# Fibonacci (via gerador):
# 1
# 1
# --- Algum código no meio ---
# 2
# 3
#
# Continuando com o loop for:
# 5
# 8
# 13

Observe como o gerador permite que a função seja "pausada" e "retomada", e como os valores são produzidos sob demanda, não todos de uma vez.

Expressões Geradoras

Assim como as list comprehensions, existem as expressões geradoras, que fornecem uma sintaxe concisa para criar geradores. Elas são definidas de forma semelhante, mas usam parênteses em vez de colchetes.


# Uma list comprehension cria uma lista completa na memória
lista_quadrados = [x*x for x in range(1, 5)] # [1, 4, 9, 16]

# Uma expressão geradora cria um objeto gerador
gerador_quadrados = (x*x for x in range(1, 5))

print(f"Tipo do objeto da expressão geradora: {type(gerador_quadrados)}") # Saída: <class>

print(next(gerador_quadrados)) # Saída: 1
print(next(gerador_quadrados)) # Saída: 4
print(next(gerador_quadrados)) # Saída: 9

for quadrado in gerador_quadrados:
    print(quadrado) # Saída: 16 (continua de onde parou)
</class>

Expressões geradoras são preferíveis quando você precisa iterar sobre uma sequência apenas uma vez e a construção de uma lista completa na memória seria ineficiente.

Ferramentas de Iteração com Geradores

Python oferece funções embutidas que atuam como geradores, faciiltando operações comuns de iteração.

zip()

A função zip() recebe vários iteráveis e retorna um iterador de tuplas, onde a i-ésima tupla contém o i-ésimo elmeento de cada um dos iteráveis de entrada. A iteração para quando o iterável mais curto se esgota.


codigos_bancos = [10086, 10000, 10010, 95586]
nomes_bancos = ['Banco X', 'Banco Y', 'Banco Z'] # Mais curto que codigos_bancos

for dados_combinados in zip(codigos_bancos, nomes_bancos):
    print(dados_combinados)
# Saída:
# (10086, 'Banco X')
# (10000, 'Banco Y')
# (10010, 'Banco Z')

Podemos criar uma versão simplificada de zip usando um gerador para entender seu funcionamento interno:


def meu_zip_simples(it_a, it_b):
    iterador_a = iter(it_a)
    iterador_b = iter(it_b)
    while True:
        try:
            val_a = next(iterador_a)
            val_b = next(iterador_b)
            yield (val_a, val_b)
        except StopIteration:
            break

print("\nResultado do meu_zip_simples:")
telefones = [111, 222, 333, 444]
usuarios = ['Alice', 'Bob', 'Charlie']

for par in meu_zip_simples(telefones, usuarios):
    print(par)
# Saída:
# Resultado do meu_zip_simples:
# (111, 'Alice')
# (222, 'Bob')
# (333, 'Charlie')

enumerate()

A função enumerate() adiciona um contador a um iterável e o retorna como um objeto enumerado. Isso pode ser usado diretamente em laços for para obter o índice e o valor de cada item.


frutas_disponiveis = ['maçã', 'banana', 'laranja']
for indice, fruta in enumerate(frutas_disponiveis):
    print(f"Índice {indice}: {fruta}")

print("\nCom start personalizado:")
for indice_personalizado, fruta_item in enumerate(frutas_disponiveis, start=1):
    print(f"Item {indice_personalizado}: {fruta_item}")
# Saída:
# Índice 0: maçã
# Índice 1: banana
# Índice 2: laranja
#
# Com start personalizado:
# Item 1: maçã
# Item 2: banana
# Item 3: laranja

Uma implementação geradora de enumerate demonstraria o conceito:


def meu_enumerate_simples(iterable_obj, inicio=0):
    iterador_interno = iter(iterable_obj)
    contador_idx = inicio
    while True:
        try:
            item_atual = next(iterador_interno)
            yield (contador_idx, item_atual)
            contador_idx += 1
        except StopIteration:
            break

print("\nResultado do meu_enumerate_simples:")
produtos = ['monitor', 'teclado', 'mouse']
for idx, produto in meu_enumerate_simples(produtos, inicio=10):
    print(f"ID {idx}: {produto}")
# Saída:
# Resultado do meu_enumerate_simples:
# ID 10: monitor
# ID 11: teclado
# ID 12: mouse

Tags: Python iterators generators yield IteratorProtocol

Publicado em 7-7 03:36