Iteradores e Geradores em Python

# Exemplo de lista comum
l1 = [2, 4, 5, 6]
l2 = [x + 1 for x in l1]
print(type(l2))  # Saída: <class 'list'>
print(l2)       # Saída: [3, 5, 6, 7]

A expressão list comprehension acima cria uma nova lista na memória. Para iteráveis volumosos, isso pode ser ineficiente:

# Exemplo com um grande iterável (comentado para evitar consumo de memória)
# l3 = [x + 1 for x in range(1000000)]
# print(l3) # Cria uma lista com um milhão de elementos

Para lidar com grandes volumes de dados de forma mais eficiente em termos de memória, podemos usar expressões geradoras (generator expressions), que se assemelham a list comprehensions, mas usam parênteses em vez de colchetes.

# Expressão geradora
l4 = (x + 1 for x in range(1000000))
print(l4)  # Saída: <generator object <genexpr> at 0x...>

# Recuperando valores um por um usando next()
l5 = (x + 1 for x in [2, 4, 5, 6])
print(next(l5)) # Saída: 3
print(next(l5)) # Saída: 5
print(next(l5)) # Saída: 6
print(next(l5)) # Saída: 7

# Tentar obter o próximo valor após o último elemento gera um StopIteration
# print(next(l5)) # Descomentar esta linha causaria um StopIteration

Para evitar a exceção StopIteration, podemos iterar sobre o gerador usando um loop for, que lida com o fim da iteração de forma mais elegante.

l6 = (x for x in [2, 4, 5, 6])
print(type(l6))  # Saída: <class 'generator'>
for i in l6:
    print(i)  # Saída: 2, 4, 5, 6 (cada um em uma nova linha)

Cuidado: Evite chamar next() explicitamente dentro de um loop for que já está iterando sobre o gerador, pois isso consumirá os elementos duas vezes.

l7 = (x for x in [2, 4, 5, 6])
# O loop for já chama next() implicitamente. Chamar print(next(l7)) novamente consome o próximo item.
# for i in l7:
#     print(next(l7)) # Isto não é o comportamento esperado

Geradores em Funções: A Palavra-chave yield

A palavra-chave yield transforma uma função regular em uma função geradora. Em vez de retornar um valor e terminar, yield pausa a execução da função, retorna um valor e salva o estado da função para que possa ser retomada posteriormante.

Exemplo de função geradora para a sequência de Fibonacci:

def fibonacci_generator(n):  # Gera os primeiros n números de Fibonacci
    a, b = 0, 1
    count = 0
    while count < n:
        yield b
        a, b = b, a + b
        count += 1

# Criando um gerador
fib_gen = fibonacci_generator(5)
print(fib_gen)  # Saída: <generator object fibonacci_generator at 0x...>
print(type(fib_gen)) # Saída: <class 'generator'>

# Obtendo valores um por um
print(next(fib_gen)) # Saída: 1
print(next(fib_gen)) # Saída: 1
print(next(fib_gen)) # Saída: 2
print(next(fib_gen)) # Saída: 3
print(next(fib_gen)) # Saída: 5

# O próximo next() levantaria StopIteration
# print(next(fib_gen))

Fluxo de Execução com yield

Vamos analisar o fluxo de execução de uma função geradora mais detalhadamente:

def simple_generator(n):
    for i in range(n):
        print(f"Antes do yield {i}")
        yield i
        print(f"Depois do yield {i}")
    print("Função concluída")

gen = simple_generator(3)
print(type(gen)) # Saída: <class 'generator'>

# Primeira chamada next()
print("Chamando next(gen) pela primeira vez:")
print(next(gen))
# Saída:
# Chamando next(gen) pela primeira vez:
# Antes do yield 0
# 0

# Segunda chamada next()
print("Chamando next(gen) pela segunda vez:")
print(next(gen))
# Saída:
# Chamando next(gen) pela segunda vez:
# Depois do yield 0
# Antes do yield 1
# 1

# Terceira chamada next()
print("Chamando next(gen) pela terceira vez:")
print(next(gen))
# Saída:
# Chamando next(gen) pela terceira vez:
# Depois do yield 1
# Antes do yield 2
# 2

# Quarta chamada next()
print("Chamando next(gen) pela quarta vez:")
try:
    print(next(gen))
except StopIteration as e:
    print(f"Exceção StopIteration capturada: {e}")
# Saída:
# Chamando next(gen) pela quarta vez:
# Depois do yield 2
# Função concluída
# Exceção StopIteration capturada: None

Pontos Chave sobre yield:

  • Transforma uma função em um gerador.
  • Pausa a execução na instrução yield e retorna o valor.
  • Retoma a execução a partir do ponto de interrupção na próxima chamada.
  • A execução continua até o final da função ou até que outro yield seja encontrado.
  • Se a função geradora tiver uma instrução return sem valor, ou se terminar sem return, o valor da exceção StopIteration será None.

Acessando o Valor de Retorno de um Gerador

Para acessar o valor de retorno de uma função geradora (o valor especificado na instrução return), você pode capturar a exceção StopIteration.

def generator_with_return(n):
    for i in range(n):
        yield i
    return "Processo concluído com sucesso!"

gen_return = generator_with_return(3)

while True:
    try:
        value = next(gen_return)
        print(f"Valor obtido: {value}")
    except StopIteration as e:
        print(f"Gerador finalizado. Valor de retorno: {e.value}")
        break
# Saída:
# Valor obtido: 0
# Valor obtido: 1
# Valor obtido: 2
# Gerador finalizado. Valor de retorno: Processo concluído com sucesso!

Tags: Python iteradores geradores yield expressões geradoras

Publicado em 7-23 18:49