List, dict, set comprehension em Python: escrever conciso e legível
1. Visão geral
Comprehension é uma sintaxe característica do Python. Ela permite criar list, dict ou set a partir de um iterável em apenas uma linha. Escrito corretamente, o código fica mais conciso e claro do que um for. Abusando, o código vira uma linha longa e incompreensível.
Este artigo passa por list, dict, set comprehension e generator expression através de exemplos práticos. Após ler, você saberá quando usar comprehension, quando voltar para for e como escrever testes para lógica de transformação de dados. Todos os exemplos rodam em Python 3.12.
2. List comprehension
2.1. Sintaxe básica
Suponha que você tenha uma lista de pedidos e precise extrair o valor total de cada um. A forma tradicional usando for seria assim:
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer: str
total: float
status: str
orders = [
Order(1, "An", 250_000, "PAID"),
Order(2, "Bình", 90_000, "CANCELLED"),
Order(3, "An", 1_200_000, "PAID"),
Order(4, "Chi", 450_000, "PENDING"),
]
totals = []
for order in orders:
totals.append(order.total)
print(totals)
Reescrito com list comprehension:
totals = [order.total for order in orders]
print(totals)
[250000, 90000, 1200000, 450000]
A estrutura é [expressão for elemento in iterável]. Leia da esquerda para a direita: “pegue order.total para cada order em orders”. Você não precisa declarar uma lista vazia nem chamar append. A intenção “estou criando uma nova lista” fica clara de imediato pelo [.
2.2. Filtrando elementos com if
Se quiser apenas pedidos já pagos, adicione uma cláusula if no final:
paid_ids = [order.id for order in orders if order.status == "PAID"]
print(paid_ids)
[1, 3]
if no final funciona como um filtro. Elementos que não satisfazem a condição são descartados, então a lista resultante pode ser menor que a original.
2.3. Expressão if-else no início
Quando quer transformar cada elemento baseado em uma condição em vez de filtrar, coloque if-else na expressão:
labels = ["grande" if order.total >= 500_000 else "pequeno" for order in orders]
print(labels)
['pequeno', 'pequeno', 'grande', 'pequeno']
Este é um ponto em que muitas pessoas se confundem. Você só precisa lembrar duas regras:
ifdepois deforé usado para filtrar. Esta cláusula não temelse.a if cond else bantes deforé uma expressão condicional. Esta cláusula exigeelsee mantém a mesma quantidade de elementos.
Escrever [x for x in data if cond else y] causará SyntaxError.
3. Dict comprehension
Dict comprehension usa chaves {} com pares key: value. Por exemplo, você precisa consultar pedidos rapidamente pelo id:
orders_by_id = {order.id: order for order in orders}
print(orders_by_id[3])
Order(id=3, customer='An', total=1200000, status='PAID')
Você também pode transformar um dict existente. Por exemplo, aplicar 10% de imposto a uma tabela de preços:
prices = {"café": 30_000, "chá_com_leite": 45_000, "pão": 20_000}
prices_with_vat = {name: round(price * 1.1) for name, price in prices.items()}
print(prices_with_vat)
{'café': 33000, 'chá_com_leite': 49500, 'pão': 22000}
prices.items() retorna pares (key, value), e você desempacota direto no for.
Uma dica útil é inverter o dict:
status_codes = {"PAID": 1, "PENDING": 2, "CANCELLED": 3}
code_to_status = {code: status for status, code in status_codes.items()}
print(code_to_status[2])
PENDING
Nota: Se dois elementos resultarem na mesma key, o elemento posterior sobrescreverá silenciosamente o anterior. Por exemplo,
{order.customer: order.total for order in orders}mantém apenas o último pedido de “An” (1.200.000), e o de 250.000 desaparece sem nenhum aviso. Quando precisar agrupar, usedefaultdictcomfor(veja a seção 7).
4. Set comprehension
Set comprehension também usa {} mas com apenas uma expressão, sem :. O resultado remove automaticamente duplicatas. Por exemplo, você quer saber quais clientes fizeram pedidos:
customers = {order.customer for order in orders}
print(sorted(customers))
['An', 'Bình', 'Chi']
Cada cliente aparece apenas uma vez, mesmo que “An” tenha dois pedidos. A ordem dentro do set não é garantida, então o exemplo usa sorted ao imprimir.
Set comprehension é muito útil quando você precisa verificar associação rapidamente:
banned_emails = {"spam@x.com", "Fake@Y.com"}
normalized = {email.strip().lower() for email in banned_emails}
print("fake@y.com" in normalized)
True
Nota:
{}vazio é um dict, não um set. Para criar um set vazio, você precisa escreverset().
5. Múltiplos for e comprehension aninhada
Você pode escrever múltiplos for em uma comprehension. A ordem de leitura é exatamente igual a quando você escreve loops for aninhados de cima para baixo:
carts = {
"An": ["café", "pão"],
"Chi": ["chá_com_leite"],
}
items = [(customer, item) for customer, products in carts.items() for item in products]
print(items)
[('An', 'café'), ('An', 'pão'), ('Chi', 'chá_com_leite')]
A comprehension acima é equivalente ao seguinte código:
items = []
for customer, products in carts.items():
for item in products:
items.append((customer, item))
O caso mais comum é achatar (flatten) uma lista aninhada:
matrix = [[1, 2, 3], [4, 5, 6]]
flat = [value for row in matrix for value in row]
print(flat)
[1, 2, 3, 4, 5, 6]
Forma de lembrar: o loop for mais externo vem primeiro. Se você precisar parar e ler uma segunda vez para entender a ordem, é sinal de que deve dividir em loops for comuns.
6. Generator expression: quando não precisa de uma list
Se trocar [] por (), você terá uma generator expression. Python não cria uma list em memória. Os valores são gerados sob demanda conforme o código os consome.
revenue = sum(order.total for order in orders if order.status == "PAID")
print(revenue)
1450000
Quando o generator é o único argumento de uma função, você pode omitir um par de parênteses. Isso é adequado para sum, any, all, max, min:
has_big_order = any(order.total > 1_000_000 for order in orders)
all_positive = all(order.total > 0 for order in orders)
print(has_big_order, all_positive)
True True
any para assim que encontra o primeiro elemento que satisfaz a condição. Portanto, com dados grandes, o generator economiza memória e é mais rápido do que any([...]).
Nota: Você pode iterar um generator apenas uma vez. A segunda iteração não retornará nada:
paid = (o for o in orders if o.status == "PAID")
print(len(list(paid)))
print(len(list(paid)))
2
0
Se precisar usar o resultado várias vezes, use list comprehension.
7. Mantendo comprehension legível
7.1. Nomeando funções para lógica complexa
Condições longas tornam a comprehension difícil de ler:
vip = [o.customer for o in orders if o.status == "PAID" and o.total >= 1_000_000 and not o.customer.startswith("test")]
Você pode separar a condição em uma função com nome significativo:
def is_vip_order(order: Order) -> bool:
return (
order.status == "PAID"
and order.total >= 1_000_000
and not order.customer.startswith("test")
)
vip = [order.customer for order in orders if is_vip_order(order)]
print(vip)
['An']
Agora a comprehension lê como uma frase, e a lógica de negócio tem seu próprio lugar para ser testada.
7.2. Quebras de linha sensatas
Comprehension não precisa estar em uma única linha. Dentro dos parênteses, você pode quebrar linhas à vontade:
summary = {
order.id: order.total
for order in orders
if order.status != "CANCELLED"
}
print(summary)
{1: 250000, 3: 1200000, 4: 450000}
Cada linha tem um papel: expressão, fonte, filtro.
7.3. Walrus operator para evitar calcular duas vezes
Às vezes você quer filtrar e usar o resultado do mesmo cálculo. O operador := (Python 3.8+) permite calcular apenas uma vez:
def discount_for(order: Order) -> float:
return order.total * 0.05 if order.total >= 400_000 else 0
discounts = {
order.id: d
for order in orders
if (d := discount_for(order)) > 0
}
print(discounts)
{3: 60000.0, 4: 22500.0}
Use o walrus com parcimônia. A variável d vai “vazar” para o escopo fora da comprehension. Além disso, muitos leitores ainda não estão familiarizados com essa sintaxe.
8. Testando lógica de transformação de dados
Comprehension geralmente fica dentro de funções de transformação de dados, então você deve testá-las como qualquer outra lógica. Primeiro, instale pytest:
pip install pytest
Suponha que você tenha um módulo reports.py:
# reports.py
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer: str
total: float
status: str
def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
customers = {o.customer for o in orders if o.status == "PAID"}
return {
c: sum(o.total for o in orders if o.customer == c and o.status == "PAID")
for c in customers
}
E o arquivo de teste correspondente:
# test_reports.py
from reports import Order, revenue_by_customer
def test_revenue_only_counts_paid_orders():
orders = [
Order(1, "An", 250_000, "PAID"),
Order(2, "An", 1_200_000, "PAID"),
Order(3, "Bình", 90_000, "CANCELLED"),
Order(4, "Chi", 450_000, "PAID"),
]
result = revenue_by_customer(orders)
assert result == {"An": 1_450_000, "Chi": 450_000}
assert "Bình" not in result
def test_empty_orders_returns_empty_dict():
assert revenue_by_customer([]) == {}
pytest -q
.. [100%]
2 passed in 0.01s
O primeiro teste afirma duas coisas: pedidos cancelados não são contados, e clientes com apenas pedidos cancelados não aparecem no resultado. O segundo teste verifica o caso extremo com uma lista vazia.
A função acima está correta mas tem complexidade O(n × número de clientes) porque cada cliente varre todos os orders. Com dados grandes, um loop for com defaultdict é mais rápido e legível:
from collections import defaultdict
def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
totals: dict[str, float] = defaultdict(float)
for order in orders:
if order.status == "PAID":
totals[order.customer] += order.total
return dict(totals)
Os dois testes antigos continuam passando sem modificação. Esse é precisamente o benefício de ter testes: você pode refatorar com confiança entre comprehension e for ou vice-versa.
9. Quando NÃO usar comprehension
- Quando precisa de side effects. Não escreva
[print(o) for o in orders]ou[db.save(o) for o in orders]. Isso cria uma listaNonesem sentido e engana o leitor pensando que você precisa do resultado. Use um loopfor. - Quando aninhado além de dois níveis ou com múltiplos
ifintercalados. Três loopsforem uma linha quase sempre são mais difíceis de ler do que a versão separada. - Quando precisar agrupar ou acumular como somar ou contar. Use
defaultdict,Counterou um loopfor. - Quando precisar tratar exceções para cada elemento. Comprehension não pode conter
try/except. Você deve extrair em uma função separada e chamá-la na comprehension, ou usar um loopfor. - Quando os dados são muito grandes e você os itera apenas uma vez. Então use generator expression em vez de list.
Nota: Em Python 3, a variável de loop do comprehension não vazará. Após
[x for x in range(3)], o nomexnão existe no escopo externo, ao contrário de um loopforcomum. A única exceção é a variável atribuída com:=conforme mencionado na seção 7.3.
10. Conclusão
Comprehension ajuda você a expressar “criar uma nova coleção de uma coleção existente” de forma concisa:
[...]para list,{k: v ...}para dict,{...}para set, e(...)para generator.ifdepois deforé usado para filtrar, enquantoa if cond else bantes deforé para transformar.- Extraia condições complexas em funções com nomes significativos, e quebre linhas quando a expressão for longa.
- Use generator expression com
sum,any,allpara economizar memória. - Volte para
forquando há side effects, quando precisar agrupar, tratar exceções, ou quando a comprehension ficar difícil de ler.
A regra mais simples: se um colega precisar ler sua comprehension duas vezes para entender, reescreva com um loop for. Código conciso só tem valor se permanecer legível.