W3docs

Generators e yield em Python

Aprenda generators e a palavra-chave yield em Python com exemplos práticos sobre funções geradoras, expressões, send() e casos de uso reais.

Um generator é um tipo especial de iterador que produz valores um de cada vez, sob demanda, em vez de calculá-los todos de uma vez. Os generators são definidos usando a sintaxe comum de função com yield no lugar de return. Eles são a solução idiomática em Python para sequências grandes ou infinitas, onde construir uma lista completa desperdiçaria memória ou tempo.

Este capítulo aborda a palavra-chave yield, funções geradoras versus listas, expressões geradoras, como enviar valores para um generator, encadeamento de generators e padrões do mundo real.

O que é um Generator?

Quando Python chama uma função regular, ela executa o corpo até o fim e retorna um valor. Quando Python chama uma função geradora, ela não executa o corpo de imediato — retorna um objeto generator. Cada vez que você chama next() nesse objeto, a execução é retomada de onde parou (na instrução yield), roda até o próximo yield e é suspensa novamente.

def count_up(start, stop):
    while start <= stop:
        yield start        # pause here, emit the value
        start += 1

gen = count_up(1, 3)
print(next(gen))   # 1
print(next(gen))   # 2
print(next(gen))   # 3
# next(gen) would now raise StopIteration

Mecânica essencial:

  • O corpo da função não é executado até a primeira chamada de next().
  • Variáveis locais e o ponteiro de instrução são preservados entre as chamadas.
  • Quando o corpo da função termina (ou encontra um return simples), Python levanta StopIteration automaticamente.
  • Um laço for chama next() por você e para corretamente no StopIteration.

A Palavra-chave yield

yield é a única sintaxe que distingue uma função geradora de uma função comum. Você pode usar yield em qualquer lugar onde return pudesse aparecer, inclusive dentro de laços, condicionais e blocos try/except.

yield vs return

returnyield
Tipo de funçãoRegularGeradora
Execução após chamadaRoda até o fimPausa no yield
Estado entre chamadasDescartadoPreservado
Múltiplos valoresUm (ou uma tupla)Um por yield, sequencialmente
Memória para grandes dadosArmazena todos os valoresArmazena um valor por vez

yield Suspende, Não Termina

def three_things():
    print("about to yield first")
    yield "first"
    print("about to yield second")
    yield "second"
    print("about to yield third")
    yield "third"
    print("generator exhausted")

for item in three_things():
    print("got:", item)

Saída:

about to yield first
got: first
about to yield second
got: second
about to yield third
got: third
generator exhausted

Observe as instruções print entre os yields — código normal é executado entre cada suspensão.

Funções Geradoras vs Listas

Considere gerar os primeiros n quadrados. Usando uma lista:

def squares_list(n):
    result = []
    for i in range(1, n + 1):
        result.append(i * i)
    return result

print(squares_list(5))   # [1, 4, 9, 16, 25]

Usando um generator:

def squares_gen(n):
    for i in range(1, n + 1):
        yield i * i

gen = squares_gen(5)
print(list(gen))         # [1, 4, 9, 16, 25]

Ambos produzem os mesmos valores, mas a versão com generator:

  • Usa memória O(1) independentemente de n (a versão com lista usa O(n))
  • Começa a produzir valores imediatamente, sem esperar construir toda a coleção
  • Pode representar sequências infinitas (uma lista não pode)

Quando Escolher um Generator

Use um generator quando:

  • Você precisa iterar pelos valores apenas uma vez.
  • A sequência é grande o suficiente para que manter tudo na memória seja relevante.
  • Você está construindo um pipeline de dados (um generator alimenta outro).
  • A sequência é potencialmente infinita (por exemplo, lendo linhas de log de um arquivo ao vivo).

Use uma lista quando:

  • Você precisa de acesso aleatório por índice.
  • Você precisa iterar a mesma sequência várias vezes.
  • Você precisa de len(), fatiamento ou ordenação no lugar.

Expressões Geradoras

Uma expressão geradora está para generators assim como uma compreensão de lista está para listas. A sintaxe é idêntica, exceto pelo uso de parênteses em vez de colchetes:

# List comprehension — builds the full list immediately
squares_list = [x * x for x in range(1, 6)]

# Generator expression — lazy, produces one value at a time
squares_gen = (x * x for x in range(1, 6))

print(type(squares_list))   # <class 'list'>
print(type(squares_gen))    # <class 'generator'>

print(list(squares_gen))    # [1, 4, 9, 16, 25]

Expressões geradoras são mais úteis quando passadas diretamente a uma função que consome um iterável:

total = sum(x * x for x in range(1, 101))   # sum of squares 1..100
print(total)   # 338350

Não são necessários parênteses extras quando a expressão geradora é o único argumento de uma chamada de função.

Filtrando com Expressões Geradoras

evens = (x for x in range(20) if x % 2 == 0)
print(list(evens))   # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Generators Infinitos

Como um generator produz valores de forma preguiçosa (lazy), ele pode representar uma sequência sem fim. O exemplo clássico é um contador infinito:

def counter(start=0):
    n = start
    while True:
        yield n
        n += 1

gen = counter(10)
print(next(gen))   # 10
print(next(gen))   # 11
print(next(gen))   # 12

Para consumir apenas parte de um generator infinito, use itertools.islice ou saia de um laço:

import itertools

gen = counter(1)
first_five = list(itertools.islice(gen, 5))
print(first_five)   # [1, 2, 3, 4, 5]

Um generator infinito prático — a sequência de Fibonacci:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

yield from — Delegando a um Sub-Generator

yield from permite que um generator delegue a outro iterável, encaminhando cada valor de forma transparente:

def first_part():
    yield 1
    yield 2

def second_part():
    yield 3
    yield 4

def combined():
    yield from first_part()
    yield from second_part()

print(list(combined()))   # [1, 2, 3, 4]

yield from também funciona com qualquer iterável, não apenas generators:

def flatten(nested):
    for sublist in nested:
        yield from sublist

data = [[1, 2], [3, 4], [5, 6]]
print(list(flatten(data)))   # [1, 2, 3, 4, 5, 6]

yield from é mais limpo do que um laço for aninhado sobre o sub-iterável e encaminha corretamente chamadas de send() e throw() ao generator delegado (importante para padrões de coroutine).

Enviando Valores para um Generator

Generators são canais bidirecionais. O método .send(value) retoma o generator e passa um valor de volta como resultado da expressão yield:

def accumulator():
    total = 0
    while True:
        value = yield total   # yield sends total out; receives value in
        if value is None:
            break
        total += value

gen = accumulator()
next(gen)          # prime the generator (advance to first yield)
print(gen.send(10))   # 10
print(gen.send(20))   # 30
print(gen.send(5))    # 35

Regras para .send():

  1. Você deve chamar next(gen) (ou gen.send(None)) uma vez para avançar o generator até o primeiro yield antes de enviar um valor diferente de None.
  2. send(None) é equivalente a next().
  3. O valor enviado torna-se o resultado da expressão yield no lado esquerdo.

Estado do Generator e Esgotamento

Um objeto generator tem um ciclo de vida com quatro estados:

EstadoDescrição
CriadoFunção geradora chamada, corpo ainda não iniciado
Em execuçãoExecutando no momento (dentro de uma chamada next() ou send())
SuspensoPausado em um yield; será retomado no próximo next()
FechadoCorpo finalizado ou .close() chamado; levanta StopIteration

Uma vez esgotado, reiterar um generator não produz nada:

gen = (x for x in range(3))
print(list(gen))   # [0, 1, 2]
print(list(gen))   # []  — already exhausted

Se você precisar iterar a saída de um generator mais de uma vez, converta-o em uma lista primeiro ou recrie o generator.

return Dentro de um Generator

Uma instrução return dentro de um generator encerra a iteração de forma limpa. O valor passado ao return torna-se o atributo value da exceção StopIteration (raramente usado diretamente, mas importante para a delegação com yield from):

def limited():
    yield 1
    yield 2
    return "done"    # StopIteration.value = "done"

gen = limited()
print(next(gen))   # 1
print(next(gen))   # 2
try:
    next(gen)
except StopIteration as e:
    print(e.value)  # done

Padrões do Mundo Real

Lendo um Arquivo Grande Linha por Linha

def read_lines(filepath):
    with open(filepath) as f:
        for line in f:
            yield line.rstrip("\n")

# Memory usage stays constant regardless of file size
for line in read_lines("/etc/hosts"):
    if line.startswith("#"):
        continue
    print(line)

Construindo um Pipeline de Dados

Generators se compõem naturalmente em pipelines onde cada etapa transforma o fluxo:

def integers(n):
    for i in range(1, n + 1):
        yield i

def only_even(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def squared(nums):
    for n in nums:
        yield n * n

# Compose: even squares from 1..20
pipeline = squared(only_even(integers(20)))
print(list(pipeline))
# [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

Cada etapa é preguiçosa (lazy) — os valores fluem pelo pipeline um de cada vez, sem construir listas intermediárias.

Dividindo um Iterável em Lotes

def chunks(iterable, size):
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == size:
            yield chunk
            chunk = []
    if chunk:
        yield chunk

data = list(range(10))
for batch in chunks(data, 3):
    print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]

Generators vs Iteradores vs Compreensões

RecursoClasse iteradoraFunção geradoraExpressão geradora
SintaxeClasse com __iter__/__next__def + yield(expr for x in ...)
VerbosidadeAltaBaixaMuito baixa
Gerenciamento de estadoManualAutomáticoAutomático
Lógica com múltiplas instruçõesSimSimNão (expressão única)
Sequências infinitasSimSimSim
Legibilidade para lógica complexaSimSimNão

Para qualquer coisa além de uma transformação ou filtro simples, uma função geradora é mais legível do que uma expressão geradora. Para iteração complexa com estado, uma função geradora é quase sempre preferível a escrever uma classe iteradora completa — veja Iteradores Python para a abordagem baseada em classes.

Expressões geradoras combinam naturalmente com compreensões de lista e compreensões de dicionário/conjunto. Decoradores também podem envolver funções geradoras para adicionar comportamento de cache ou rastreamento.

Prática

Prática
Which of the following statements about Python generators are correct?
Which of the following statements about Python generators are correct?
Was this page helpful?