Python Dict e Set Comprehension

Na aula List Comprehension, você viu como montar uma lista numa linha só: [n * 2 for n in numeros]. A mesma ideia vale para dicionários e conjuntos. Com a dict comprehension, a Ana monta a tabela de preços com aumento, o estoque sem os produtos zerados ou o boletim da turma, cada um numa linha. Com a set comprehension, ela monta um conjunto do mesmo jeito.

Você vai aprender
  • A escrever uma dict comprehension: {chave: valor for item in sequência}.
  • A transformar os valores de um dicionário com items().
  • A filtrar pares com if no fim.
  • A montar um dicionário a partir de duas listas com zip(), e a inverter chaves e valores.
  • A set comprehension: {x for x in …}.
  • Os erros reais e a pegadinha que troca o dicionário por um conjunto sem avisar.

A ideia: a mesma receita, outra forma

Pense numa receita que diz: "para cada fruta da cesta, escreva numa etiqueta o nome da fruta e, ao lado, quantas letras ela tem". Essa frase descreve um dicionário inteiro sem você precisar listar cada fruta. A comprehension é essa frase escrita em Python: o que vai na etiqueta (chave: valor), e de onde vêm os itens (for … in …).

Para montarComprehensionEnvolve com
Uma lista[n * 2 for n in numeros]Colchetes
Um dicionário{n: n * 2 for n in numeros}Chaves, com chave: valor
Um conjunto{n * 2 for n in numeros}Chaves, com um valor só

1. A primeira dict comprehension

Primeiro, o jeito que você já conhece, com for: um dicionário com cada número de 1 a 5 e o seu quadrado.

quadrados = {}
for n in range(1, 6):
    quadrados[n] = n * n
print(quadrados)
$ python3 dc_for.py
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

Agora o mesmo resultado, numa linha:

quadrados = {n: n * n for n in range(1, 6)}
print(quadrados)
$ python3 dc_basico.py
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
{ … }
As chaves dizem que o resultado vai ser um dicionário (ou um conjunto; o dois-pontos decide).
n: n * n
O par que vai ser criado a cada volta: a chave n, o dois-pontos, e o valor n * n. É o que, no for, estava em quadrados[n] = n * n.
for n in range(1, 6)
De onde vêm os itens: o mesmo for de sempre, sem os dois-pontos do fim e sem o bloco recuado.

Leia da esquerda para a direita como "o dicionário de n para n * n, para cada n de 1 a 5". A chave e o valor podem vir de qualquer conta sobre o item. Para cada fruta, o tamanho do nome:

frutas = ["manga", "caju", "acerola"]
tamanhos = {fruta: len(fruta) for fruta in frutas}
print(tamanhos)
$ python3 dc_palavras.py
{'manga': 5, 'caju': 4, 'acerola': 7}

2. Transformando um dicionário: items()

O uso mais comum é partir de um dicionário que já existe e criar outro, mudando os valores (ou as chaves). Percorra com items(), que entrega chave e valor (aula Percorrendo um Dicionário):

precos = {"pão": 0.50, "leite": 4.75, "café": 18.90}
com_aumento = {produto: preco + 1 for produto, preco in precos.items()}
print(com_aumento)
em_centavos = {produto: round(preco * 100) for produto, preco in precos.items()}
print(em_centavos)
maiusculas = {produto.upper(): preco for produto, preco in precos.items()}
print(maiusculas)
$ python3 dc_items.py
{'pão': 1.5, 'leite': 5.75, 'café': 19.9}
{'pão': 50, 'leite': 475, 'café': 1890}
{'PÃO': 0.5, 'LEITE': 4.75, 'CAFÉ': 18.9}
for produto, preco in precos.items()
A cada volta, um par do dicionário original, já desempacotado.
produto: preco + 1
Mesma chave, valor novo: um real a mais.
produto: round(preco * 100)
O preço em centavos, arredondado para inteiro.
produto.upper(): preco
Agora é a chave que muda: o nome em maiúsculas.

Nos três casos, precos continua igual: a comprehension sempre cria um dicionário novo.

Missão: chegou um caminhão e o estoque de tudo dobrou. Troque o {} por uma dict comprehension que cria dobro com cada quantidade vezes 2.

estoque = {"pão": 40, "leite": 12, "café": 8}
dobro = {}
print(dobro)
estoque = {"pão": 40, "leite": 12, "café": 8}
dobro = {produto: qtd * 2 for produto, qtd in estoque.items()}
print(dobro)

3. Filtrando: o if no fim

Como na list comprehension, um if depois do for escolhe quais itens entram. Só os produtos que ainda têm estoque:

estoque = {"pão": 40, "leite": 0, "café": 8, "bolo": 0}
tem = {produto: qtd for produto, qtd in estoque.items() if qtd > 0}
print(tem)
acabou = [produto for produto, qtd in estoque.items() if qtd == 0]
print(acabou)
$ python3 dc_if.py
{'pão': 40, 'café': 8}
['leite', 'bolo']

A primeira linha cria um dicionário só com os pares em que qtd > 0. A segunda mostra que dá para sair de um dicionário e chegar numa lista: com colchetes, o resultado é uma lista dos nomes que zeraram.

Remover itens sem o RuntimeError

Na aula Percorrendo um Dicionário, apagar chaves dentro do for deu RuntimeError: dictionary changed size during iteration. A comprehension com if é a outra saída: em vez de apagar do dicionário, você cria um novo só com o que fica, e guarda na mesma variável: estoque = {p: q for p, q in estoque.items() if q > 0}.

if e else no valor

Quando todos os itens entram, mas o valor depende de uma condição, use o if em uma linha (aula if em uma Linha) na posição do valor:

notas = {"Ana": 9, "Bia": 6, "Caio": 10}
situacao = {nome: ("passou" if nota >= 7 else "recuperação") for nome, nota in notas.items()}
print(situacao)
$ python3 dc_if_else.py
{'Ana': 'passou', 'Bia': 'recuperação', 'Caio': 'passou'}
Onde fica o ifO que fazExemplo
No fim, depois do forFiltra: quem não passa no teste não entra{n: q for n, q in d.items() if q > 0}
No valor, com elseEscolhe o valor; todos entram{n: ("sim" if q > 0 else "não") for n, q in d.items()}

Missão: crie passaram com uma dict comprehension que fica só com os alunos de nota 7 ou mais.

notas = {"Ana": 9, "Bia": 6, "Caio": 10, "Davi": 5}
passaram = notas
print(passaram)
notas = {"Ana": 9, "Bia": 6, "Caio": 10, "Davi": 5}
passaram = {nome: nota for nome, nota in notas.items() if nota >= 7}
print(passaram)

4. Duas listas viram um dicionário

A Ana tem os nomes numa lista e as notas noutra, na mesma ordem. O zip() (aula enumerate() e zip()) anda pelas duas ao mesmo tempo e entrega os pares:

nomes = ["Ana", "Bia", "Caio"]
notas = [9, 6, 10]
boletim = {nome: nota for nome, nota in zip(nomes, notas)}
print(boletim)
print(dict(zip(nomes, notas)))
$ python3 dc_zip.py
{'Ana': 9, 'Bia': 6, 'Caio': 10}
{'Ana': 9, 'Bia': 6, 'Caio': 10}

A segunda linha mostra um atalho: quando você não vai mudar nada nos pares, dict(zip(nomes, notas)) faz o mesmo, mais curto.

Invertendo chaves e valores

Trocando a ordem no par, a chave vira valor e o valor vira chave. A lista de ramais da firma, ao contrário: de quem é o ramal 202?

ramais = {"Ana": 201, "Bia": 202, "Caio": 203}
dono = {ramal: nome for nome, ramal in ramais.items()}
print(dono)
print(dono[202])
$ python3 dc_inverter.py
{201: 'Ana', 202: 'Bia', 203: 'Caio'}
Bia

Isso só funciona bem quando os valores não se repetem. Com valores repetidos, alguém some; veja em Quando dá errado.


5. Set comprehension

Tire a parte chave: e fique com um valor só: o resultado é um conjunto, e as repetições somem, como em todo conjunto.

restos = {n % 3 for n in range(10)}
print(restos)
palavras = ["sol", "mar", "céu", "areia", "onda"]
tamanhos = {len(p) for p in palavras}
print(tamanhos)
nomes = ["Ana", "Bia", "Caio", "Bruno", "Alice"]
iniciais = {nome[0] for nome in nomes}
print(sorted(iniciais))
$ python3 dc_set.py
{0, 1, 2}
{3, 4, 5}
['A', 'B', 'C']
{n % 3 for n in range(10)}
O resto da divisão por 3 de cada número de 0 a 9. São dez contas, mas só três resultados diferentes: 0, 1 e 2.
{len(p) for p in palavras}
Os tamanhos diferentes das palavras: sol, mar e céu têm 3 letras, e o 3 aparece uma vez.
{nome[0] for nome in nomes}
As iniciais diferentes. Como é um conjunto de textos, a ordem do print mudaria a cada execução; o sorted() põe em ordem alfabética.
E com parênteses?

Não existe "tuple comprehension". (n * n for n in range(4)) cria outra coisa, um gerador (aula Geradores), que produz os valores um de cada vez. Para uma tupla, passe o gerador para tuple():

gen = (n * n for n in range(4))
print(type(gen))
print(tuple(n * n for n in range(4)))
$ python3 dc_tupla.py
<class 'generator'>
(0, 1, 4, 9)

Formas diferentes de fazer a mesma coisa

ObjetivoCom forCom comprehension
Dicionário a partir de uma listad = {}
for f in frutas:
d[f] = len(f)
d = {f: len(f) for f in frutas}
Filtrar um dicionárionovo = {}
for k, v in d.items():
if v > 0:
novo[k] = v
novo = {k: v for k, v in d.items() if v > 0}
Duas listas em um dicionárioum for com zipdict(zip(nomes, notas))
Conjunto a partir de uma listas = set()
for p in palavras:
s.add(len(p))
s = {len(p) for p in palavras}

Qual usar? A comprehension quando a regra cabe numa linha que se lê com facilidade: transformar, filtrar, montar. O for comum quando há várias contas por item, print no meio, ou mais de um if/else: uma comprehension enorme é mais difícil de entender do que um laço de quatro linhas.


Quando dá errado

Vírgula no lugar do dois-pontos
precos = {"pão": 0.50, "leite": 4.75}
copia = {produto, preco for produto, preco in precos.items()}
$ python3 dc_virgula.py
  File "/home/ana/dc_virgula.py", line 2
    copia = {produto, preco for produto, preco in precos.items()}
             ^^^^^^^^^^^^^^
SyntaxError: did you forget parentheses around the comprehension target?
File "/home/ana/dc_virgula.py", line 2
Sem a palavra Traceback: é um erro de escrita, achado antes de o programa rodar.
^^^^^^^^^^^^^^
Marca produto, preco: o "alvo" da comprehension, o que ela monta a cada volta.
SyntaxError: did you forget parentheses around the comprehension target?
"Você esqueceu os parênteses em volta do alvo da comprehension?". Com a vírgula, o Python acha que você queria um conjunto de tuplas, que se escreve {(produto, preco) for …}, e sugere os parênteses.

Como consertar: para um dicionário, o que separa chave e valor é o dois-pontos: {produto: preco for …}.

Usar uma variável que o for não criou
precos = {"pão": 0.50, "leite": 4.75}
copia = {produto: preco for produto in precos}
$ python3 dc_nameerror.py
Traceback (most recent call last):
  File "/home/ana/dc_nameerror.py", line 2, in <module>
    copia = {produto: preco for produto in precos}
                      ^^^^^
NameError: name 'preco' is not defined. Did you mean: 'precos'?

O for só criou produto (percorrendo as chaves); preco não existe. O Python ainda sugere precos, que é parecido, mas não é o que se quer. Como consertar: percorra os pares, for produto, preco in precos.items(), ou leia o valor pela chave: {produto: precos[produto] for produto in precos}.

Esquecer o items()
precos = {"pão": 0.50, "leite": 4.75}
copia = {produto: preco for produto, preco in precos}
$ python3 dc_sem_items.py
Traceback (most recent call last):
  File "/home/ana/dc_sem_items.py", line 2, in <module>
    copia = {produto: preco for produto, preco in precos}
                                ^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)

O mesmo erro da aula sobre percorrer: sem items(), o for entrega só a chave "pão", e o Python tenta desempacotar suas 3 letras em 2 variáveis. Como consertar: precos.items().

Saiu um conjunto, não um dicionário: sem erro
precos = {"pão": 0.50, "leite": 4.75}
a = {produto for produto, preco in precos.items()}
b = {produto: preco for produto, preco in precos.items()}
print(type(a))
print(type(b))
$ python3 dc_set_vs_dict.py
<class 'set'>
<class 'dict'>

Sem o : preco, a linha continua válida, mas monta um conjunto só com os nomes. Nenhum erro aparece ali; ele só vai aparecer depois, quando você tentar ler a["pão"]. Como perceber: o que decide entre dicionário e conjunto é o dois-pontos. Na dúvida, print(type(…)).

Inverter com valores repetidos: alguém some
notas = {"Ana": 9, "Bia": 6, "Caio": 9}
quem_tirou = {nota: nome for nome, nota in notas.items()}
print(quem_tirou)
$ python3 dc_inverter_perde.py
{9: 'Caio', 6: 'Bia'}

A Ana sumiu. Ela e o Caio tiraram 9, e a chave 9 só pode existir uma vez: primeiro recebeu 'Ana', depois foi trocada por 'Caio', sem aviso. É a regra de sempre: chave repetida, vale a última. Como evitar: só inverta quando os valores forem únicos (ramais, matrículas). Para agrupar quem tirou cada nota, o valor precisa ser uma lista, montada com um for comum.


Referência rápida

EscritaO que faz
{x: f(x) for x in seq}Dicionário: chave x, valor f(x)
{k: g(v) for k, v in d.items()}Dicionário novo com os valores transformados
{k: v for k, v in d.items() if teste}Dicionário novo só com os pares que passam no teste
{k: (a if teste else b) for k, v in d.items()}Escolhe o valor conforme o teste
{k: v for k, v in zip(chaves, valores)} / dict(zip(…))Dicionário a partir de duas listas
{v: k for k, v in d.items()}Inverte chaves e valores (os valores precisam ser únicos)
{f(x) for x in seq}Conjunto (sem repetidos)
tuple(f(x) for x in seq)Tupla (com parênteses sozinhos é um gerador)
MensagemCausa provável
SyntaxError: did you forget parentheses around the comprehension target?Vírgula no lugar do dois-pontos entre chave e valor
NameError: name 'preco' is not definedUsou uma variável que o for da comprehension não cria
ValueError: too many values to unpack (expected 2)Faltou o .items()
<class 'set'> onde esperava dictFaltou o chave:
Um item sumiu, sem erroDuas voltas geraram a mesma chave: vale a última

Exercícios

1. O símbolo que faz o dicionário

Complete o que separa a chave do valor:

{n n * 10 for n in range(3)}

2. O resultado

Escreva, como o Python mostra, o resultado de {n: n + 1 for n in range(3)}:

3. O filtro

Complete a palavra que fica só com os pares de valor maior que 0:

{k: v for k, v in d.items() v > 0}

4. Qual o tipo de {len(p) for p in ["sol", "mar"]}?

5. {v: k for k, v in {"a": 1, "b": 1}.items()} dá o quê?

6. Para montar um dicionário a partir de nomes e notas, duas listas na mesma ordem, o jeito mais curto é:

Missão final: os nomes da turma

Missão: com a lista de nomes (repare que a Ana aparece duas vezes), crie com comprehensions: tamanhos, um dicionário de cada nome para o tamanho dele, e iniciais, o conjunto das primeiras letras. O print das iniciais já usa sorted() para sair sempre igual.

nomes = ["Ana", "Bia", "Caio", "Ana", "Bruno"]
tamanhos = {}
iniciais = set()
print(tamanhos)
print(sorted(iniciais))
nomes = ["Ana", "Bia", "Caio", "Ana", "Bruno"]
tamanhos = {nome: len(nome) for nome in nomes}
iniciais = {nome[0] for nome in nomes}
print(tamanhos)
print(sorted(iniciais))

A Ana repetida não aparece duas vezes no dicionário (a chave é única) nem conta duas vezes o "A" no conjunto.


Resumo

  • {chave: valor for item in sequência} monta um dicionário numa linha; {valor for item in sequência} monta um conjunto. O dois-pontos decide qual.
  • Com for k, v in d.items(), transforme as chaves ou os valores de um dicionário que já existe; o original não muda.
  • Um if no fim filtra; um if … else … no valor escolhe o valor.
  • dict(zip(chaves, valores)) junta duas listas; {v: k for k, v in d.items()} inverte (com valores únicos).
  • Parênteses não fazem "tuple comprehension": fazem um gerador. Use tuple(…).
  • Vírgula no lugar do dois-pontos dá SyntaxError; faltar o chave: não dá erro, mas cria um conjunto.
  • Se a regra não cabe numa linha clara, prefira o for comum.