Fundamentos

Geradores em Python: yield, iteradores e list comprehensions de listas e dicionários

Atualizado: · Skillaria.Top

Gerador em Python é um objeto que entrega valores um a um, sob demanda, sem guardar toda a sequência na memória; é criado por uma função com a palavra-chave yield ou por uma expressão geradora entre parênteses. Geradores de listas, dicionários e conjuntos (comprehensions) são uma forma curta de montar a coleção pronta em uma linha: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.

Um gerador em Python é um objeto que entrega valores um de cada vez, apenas quando são solicitados, sem manter toda a sequência na memória. Ele é criado de duas formas: por uma função com a palavra-chave yield ou por uma expressão geradora entre parênteses. Ao lado dele estão as list comprehensions, de listas, dicionários e conjuntos — uma forma curta de montar a coleção pronta em uma linha. Vamos ver como o python yield funciona, em que o gerador difere de um iterador e como não transformar uma comprehension em um texto ilegível.

Iteradores: a base do laço for

Para entender os geradores, é preciso primeiro compreender os iteradores. Um objeto iterável (iterable) é aquilo sobre o que se pode fazer um laço: lista, string, dicionário, arquivo. Um iterador é um objeto que guarda a posição atual e, ao receber next(), entrega o próximo elemento; quando os elementos acabam, lança StopIteration.

nums = [10, 20, 30]
it = iter(nums)      # obtém um iterador a partir da lista
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration

O laço for faz exatamente isso: chama iter() e depois next() até o StopIteration. Um iterador próprio pode ser escrito como classe com os métodos __iter__ e __next__:

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

print(list(Countdown(3)))  # [3, 2, 1]

Funciona, mas há muito código repetitivo. Os geradores fazem o mesmo em três linhas.

O que é um gerador em Python e como funciona o yield

Uma função geradora é uma função comum que contém yield no corpo. Sua chamada não executa o código: ela devolve um objeto gerador. Cada next() executa o corpo até o próximo yield, entrega o valor e congela a função junto com as variáveis locais. O próximo next() continua exatamente do mesmo ponto.

def countdown(start):
    while start > 0:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          # <generator object countdown at 0x...>
print(next(gen))    # 3
print(list(gen))    # [2, 1] — continuou do ponto em que parou

Quando a função chega ao fim (ou a um return), o gerador lança sozinho StopIteration, e o laço for termina sem erros. Daí a diferença principal entre yield e return: o return encerra a função e devolve um único valor, enquanto o yield a suspende e pode entregar quantos valores forem necessários.

Gerador e iterador: diferenças

Todo gerador é um iterador: ele possui __iter__ e __next__. Mas nem todo iterador é um gerador. A diferença está na forma de criação e nos recursos.

Iterador (classe)Gerador
Como criarclasse com __iter__ e __next__função com yield ou (x for x in ...)
Estadomanualmente, em atributosguardado automaticamente nas variáveis locais
StopIterationlançado por vocêaparece sozinho ao sair da função
Recursos extraso que você implementarsend(), throw(), close()

Ambos têm uma propriedade importante em comum: é possível percorrê-los só uma vez. Um novo for sobre um gerador esgotado não produz nada — é preciso criar outro.

Gerador infinito e leitura de arquivos grandes

O gerador não precisa terminar. Uma sequência infinita é segura desde que você pegue dela um número limitado de elementos:

from itertools import islice

def ids():
    n = 1
    while True:
        yield f"ID-{n:04d}"
        n += 1

print(list(islice(ids(), 3)))  # ['ID-0001', 'ID-0002', 'ID-0003']

O cenário mais prático é processar um arquivo grande linha por linha. Os geradores podem ser encadeados em um pipeline, e na memória haverá a cada momento uma única linha, e não o arquivo inteiro:

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

def only_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

for line in only_errors(read_lines("app.log")):
    print(line)

yield from e send

yield from repassa todos os valores de outro objeto iterável, dispensando um laço aninhado:

def flatten(groups):
    for g in groups:
        yield from g

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

O método send() envia um valor para dentro do gerador: ele se torna o resultado da expressão yield. Antes do primeiro send o gerador precisa ser «iniciado» com next(). Assim se escreve, por exemplo, um acumulador de média:

def averager():
    total, count = 0, 0
    avg = None
    while True:
        value = yield avg
        total += value
        count += 1
        avg = total / count

a = averager()
next(a)            # chega ao primeiro yield
print(a.send(10))  # 10.0
print(a.send(20))  # 15.0

No código de aplicação o send aparece raramente, mas é um tema frequente em entrevistas de emprego.

range é um gerador?

Não. O range é uma sequência preguiçosa: também não guarda todos os números, mas permite indexação, len(), verificação com in e percorrer quantas vezes for preciso. O gerador não faz nada disso.

r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # nas duas vezes [0, 1, 2, 3, 4]

List comprehension: gerador de listas

A list comprehension tem a forma [expressão for x in coleção]. Ela cria uma nova lista e não altera a original. Lê-se da esquerda para a direita: primeiro «o que colocar», depois «de onde tirar».

amounts = [100, 250, 400]

# laço comum
with_markup = []
for a in amounts:
    with_markup.append(a * 1.2)

# a mesma coisa com comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup)  # [120.0, 300.0, 480.0]

Apesar do nome «gerador de listas», o resultado é uma lista pronta, e não um gerador: todos os elementos são calculados imediatamente.

Lista com if e if else

É aqui que mais se confunde. A condição-filtro fica no fim e descarta elementos. A condição-escolha (operador ternário) fica no início e transforma cada elemento em um dentre dois valores, sem descartar nada.

amounts = [100, 250, 400]

big = [a for a in amounts if a > 200]                    # [250, 400]
labels = ["muito" if a > 200 else "pouco" for a in amounts]
# ['pouco', 'muito', 'muito']

# filtro e escolha juntos
odd_marks = ["par" if n % 2 == 0 else "ímpar" for n in range(6) if n > 2]
# ['ímpar', 'par', 'ímpar']

A escrita [a for a in amounts if a > 200 else 0] é erro de sintaxe: o else só existe na condição do início.

Comprehensions aninhadas e lista bidimensional

O duplo for é escrito na mesma ordem dos laços aninhados, de cima para baixo. Isso ajuda a não trocar a ordem ao juntar uma lista de listas:

groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# equivalente a:
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]

Uma matriz (lista bidimensional) é criada com uma comprehension dentro de outra. O erro clássico é multiplicar a lista: as linhas acabam sendo o mesmo objeto.

bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — as duas linhas mudaram

grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0]]

# transposição
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]

Gerador de dicionário e de conjunto

O mesmo recurso funciona para outras coleções. Chaves com dois-pontos montam um dicionário (dict comprehension), e chaves sem dois-pontos — um conjunto (set comprehension).

spent = [("comida", 500), ("táxi", 300), ("comida", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'comida', 'táxi'} — a ordem em conjuntos não é garantida

last = {cat: amount for cat, amount in spent}
print(last)        # {'comida': 200, 'táxi': 300}

squares = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}

Repare em last: quando a chave se repete, vale o último valor, e a soma de 500 se perdeu. Se for preciso somar, a comprehension não serve — escreva um laço ou use collections.Counter / defaultdict.

E a comprehension de tuplas?

Uma «tuple comprehension» separada em Python não existe. Os parênteses criam uma expressão geradora, e a tupla é obtida passando-a para tuple():

t = tuple(n * 2 for n in range(3))  # (0, 2, 4)

Expressão geradora: não guardar, mas entregar

Se trocar os colchetes por parênteses, obtém-se uma expressão geradora. Ela não calcula nada antecipadamente e entrega os valores um a um. Para um milhão de elementos, a diferença de memória é enorme:

import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list))  # cerca de 8 MB
print(sys.getsizeof(as_gen))   # algumas centenas de bytes

Expressões geradoras costumam ficar dentro de funções agregadoras. Se o gerador for o único argumento, os parênteses extras podem ser omitidos. any() e all(), além disso, param na primeira resposta e não percorrem o restante.

spent = [("comida", 500), ("táxi", 300), ("comida", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True, o resto não é verificado
biggest = max(spent, key=lambda p: p[1])    # ('comida', 500)

A regra de escolha é simples: se o resultado for necessário várias vezes, ou para indexação e len(), use lista. Se for preciso percorrer uma vez e calcular algo, use gerador.

Escopo: a variável não vaza

Pergunta que pega até programadores experientes:

x = 10
squares = [x for x in range(3)]
print(x)  # 10

A variável do laço dentro da comprehension vive no próprio escopo e não sobrescreve a externa (no Python 3 é assim; no Python 2 era diferente). Num laço for comum, depois dele x valeria 2. A partir da versão 3.12 (PEP 709) as comprehensions são embutidas na função externa e funcionam mais rápido, mas a regra de visibilidade continuou a mesma.

Desempacotamento * e ** em comprehensions: novidade do Python 3.15 (PEP 798)

O asterisco para desempacotamento já podia ser usado havia tempos em chamadas de função e em literais de lista, mas dentro de uma comprehension era SyntaxError. No Python 3.15 essa proibição foi retirada: *elemento significa «coloque aqui todos os elementos dele». Funciona em listas, conjuntos e geradores, inclusive assíncronos.

# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups]        # [1, 2, 3, 4, 5]
uniq = {*g for g in groups}        # conjunto
lazy = (*g for g in groups)        # gerador preguiçoso

O duplo asterisco faz o mesmo com dicionários. Isso é cômodo para configurações em camadas «padrão → arquivo → flags», em que cada camada seguinte sobrescreve as chaves da anterior.

# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}

Duas ressalvas do curso. O asterisco não pode ser combinado com uma expressão condicional sem parênteses: [*w if w else 0 for w in weeks] é erro de sintaxe, e a condição precisa estar entre parênteses. E esse código não roda no 3.14 ou em versões anteriores: se a biblioteca suporta versões antigas, mantenha o duplo for ou o itertools.chain.from_iterable.

Geradores assíncronos

Se uma função async def contém yield, obtém-se um gerador assíncrono. Percorre-se com async for, e dentro dele é possível aguardar a rede ou o disco com await. Também existem comprehensions assíncronas.

import asyncio

async def ticks(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    result = [i async for i in ticks(3)]
    print(result)  # [0, 1, 2]

asyncio.run(main())

Erros típicos

  • Comprehension de três andares. Dois for, dois if e um ternário numa linha ninguém lê daqui a uma semana. Se não cabe numa linha legível, escreva um laço comum: a PEP 8 dá mais valor à legibilidade do que à brevidade.
  • Nova passagem pelo gerador. Um segundo sum(gen) devolve 0, porque o gerador já está esgotado.
  • Comprehension por efeito colateral. [print(x) for x in xs] cria uma lista desnecessária de None. Para ações use um laço for.
  • Esperar uma lista de uma expressão geradora. (x for x in xs)[0] dá TypeError: o gerador não tem índices.

Exercícios com geradores para praticar

  1. Escreva um gerador fib() que produza infinitamente os números de Fibonacci e pegue os 10 primeiros com islice.
  2. Em uma única linha obtenha um dicionário «palavra → tamanho» para as palavras com mais de três letras.
  3. Calcule a soma dos quadrados dos números pares até um milhão sem criar uma lista.
  4. Escreva um gerador chunks(seq, n) que devolva pedaços da lista de n elementos.

Resumo

O iterador entrega os elementos um a um por meio de next(), e o gerador é a forma mais simples de escrever um: uma função com python yield ou uma expressão entre parênteses. Os geradores de listas, dicionários e conjuntos montam a coleção pronta em uma linha: o filtro if vai no fim, a escolha if else — no início. Precisa economizar memória ou processar um fluxo de dados uma única vez — use gerador; precisa do resultado várias vezes — use lista. Comprehensions, geradores e o novo desempacotamento do Python 3.15 são praticados no curso, no exemplo do controle de gastos.

Perguntas frequentes

Dá para usar return dentro de um gerador?

Sim. Um return sem valor apenas encerra o gerador; um return com valor o coloca no atributo value da exceção StopIteration, e esse valor é recebido pela expressão yield from.

Como transformar um gerador em lista?

Passe-o para list(): list(gen). Depois disso o gerador estará esgotado, e todos os valores ficarão na memória.

O que é mais rápido: list comprehension ou laço com append?

Em geral a comprehension é um pouco mais rápida, pois não chama o método append em cada iteração. Mas a escolha deve considerar a legibilidade, e a diferença raramente é decisiva.

Como saber se um objeto é um gerador?

Use inspect.isgenerator(obj) para o objeto ou inspect.isgeneratorfunction(f) para a função; para verificar se é um iterador, use isinstance(obj, collections.abc.Iterator).

Qual a diferença entre expressão geradora e list comprehension?

Os colchetes constroem de imediato a lista inteira na memória; os parênteses criam um gerador, que calcula os elementos sob demanda e pode ser percorrido apenas uma vez.

Aprenda este tema em vídeo

Este tema é abordado em profundidade, com prática, no curso “Python 3.15: от новичка до профи”.

Ir para o curso