Python Dicionários Aninhados
A Ana virou professora de reforço e precisa de um cadastro dos alunos: de cada um, a idade, a cidade e as notas. Um dicionário simples guarda um valor por chave. Mas o valor pode ser de qualquer tipo, inclusive outro dicionário. Quando um dicionário guarda outros dicionários (e listas) dentro dele, dizemos que eles estão aninhados, como caixas dentro de caixas. É assim que se monta um cadastro de verdade.
- A montar um cadastro: um dicionário de alunos em que cada aluno é outro dicionário.
- A ler um dado lá de dentro, um colchete por nível:
alunos["Ana"]["notas"][0]. - A acrescentar alunos, mudar a idade e lançar notas por dentro.
- A percorrer o cadastro e calcular a média de cada aluno.
- A diferença entre dicionário de dicionários e lista de dicionários.
- A mostrar o cadastro arrumado com
pprint. - Os erros reais de cada nível, e uma armadilha sem erro: dois alunos que dividem a mesma ficha.
A ideia: o fichário
Imagine um fichário de escritório. Cada gaveta tem o nome de um aluno. Dentro da gaveta há uma ficha com campos: idade, cidade, notas. E o campo "notas" é uma lista, uma nota atrás da outra. Para achar a primeira nota da Ana, você abre a gaveta "Ana", pega a ficha, olha o campo "notas" e lê a primeira da lista. Três passos, um em cada nível.
| Nível | No fichário | Em Python | Tipo |
|---|---|---|---|
| 1 | O fichário inteiro | alunos | Dicionário (chave: nome do aluno) |
| 2 | A ficha da Ana | alunos["Ana"] | Dicionário (chave: nome do campo) |
| 3 | O campo notas | alunos["Ana"]["notas"] | Lista |
| 4 | A primeira nota | alunos["Ana"]["notas"][0] | Número |
1. O cadastro de alunos
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
"Bia": {"idade": 16, "cidade": "Picos", "notas": [6, 8, 7]},
}
print(alunos["Ana"])
print(alunos["Ana"]["cidade"])
print(alunos["Ana"]["notas"])
print(alunos["Ana"]["notas"][0])
print(len(alunos))
$ python3 n_cadastro.py
{'idade': 15, 'cidade': 'Teresina', 'notas': [9, 7, 10]}
Teresina
[9, 7, 10]
9
2
- alunos = { … }
- O dicionário de fora. As chaves são os nomes dos alunos. Foi escrito com um aluno por linha, para dar para ler.
- "Ana": { … }
- O valor da chave
"Ana"é outro dicionário: a ficha dela. - "notas": [9, 7, 10]
- Dentro da ficha, o valor de
"notas"é uma lista. - alunos["Ana"]
- Um colchete: a ficha inteira da Ana.
- alunos["Ana"]["cidade"]
- Dois colchetes: da ficha da Ana, o campo cidade.
- alunos["Ana"]["notas"][0]
- Três colchetes: da ficha da Ana, a lista de notas, e dela a posição 0. Os dois primeiros são chaves (texto); o último é uma posição (número), porque ali já estamos numa lista.
- len(alunos)
- Conta só o nível de fora: 2 alunos.
Lendo da esquerda para a direita
Uma linha como alunos["Bia"]["notas"][-1] assusta no começo, mas o Python a resolve da esquerda para a direita, um colchete de cada vez. Dá para fazer o mesmo em passos, guardando cada nível numa variável:
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
"Bia": {"idade": 16, "cidade": "Picos", "notas": [6, 8, 7]},
}
ficha = alunos["Bia"]
notas = ficha["notas"]
print(ficha)
print(notas)
print(notas[-1])
$ python3 n_passos.py
{'idade': 16, 'cidade': 'Picos', 'notas': [6, 8, 7]}
[6, 8, 7]
7
notas[-1] é exatamente o mesmo que alunos["Bia"]["notas"][-1]. Quando uma linha com muitos colchetes não funcionar, quebrá-la assim, com um print em cada passo, mostra em qual nível está o problema.
Missão: troque cada "?" pela leitura certa no cadastro: a segunda nota da Bia (posição 1) e a cidade da Ana.
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
"Bia": {"idade": 16, "cidade": "Picos", "notas": [6, 8, 7]},
}
print("Segunda nota da Bia:", "?")
print("Cidade da Ana:", "?")
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
"Bia": {"idade": 16, "cidade": "Picos", "notas": [6, 8, 7]},
}
print("Segunda nota da Bia:", alunos["Bia"]["notas"][1])
print("Cidade da Ana:", alunos["Ana"]["cidade"])
2. Mudando por dentro
Tudo o que você aprendeu em Adicionando e Removendo Chaves e em Adicionando Itens funciona em qualquer nível. Você só precisa "chegar" no nível certo com os colchetes e então usar a operação de sempre:
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
"Bia": {"idade": 16, "cidade": "Picos", "notas": [6, 8, 7]},
}
alunos["Caio"] = {"idade": 15, "cidade": "Parnaíba", "notas": []}
alunos["Caio"]["notas"].append(10)
alunos["Caio"]["notas"].append(9)
alunos["Bia"]["idade"] = 17
alunos["Ana"]["notas"][1] = 8
alunos["Ana"]["email"] = "ana@exemplo.com"
for nome in alunos:
print(nome, alunos[nome])
$ python3 n_mudar.py
Ana {'idade': 15, 'cidade': 'Teresina', 'notas': [9, 8, 10], 'email': 'ana@exemplo.com'}
Bia {'idade': 17, 'cidade': 'Picos', 'notas': [6, 8, 7]}
Caio {'idade': 15, 'cidade': 'Parnaíba', 'notas': [10, 9]}
| Linha | Nível | O que fez |
|---|---|---|
alunos["Caio"] = {…} | 1 (o cadastro) | Novo aluno, com uma ficha nova e a lista de notas vazia |
alunos["Caio"]["notas"].append(10) | 3 (a lista) | Lançou uma nota no fim da lista do Caio |
alunos["Bia"]["idade"] = 17 | 2 (a ficha) | Trocou o valor de um campo que já existia |
alunos["Ana"]["notas"][1] = 8 | 4 (um item da lista) | Corrigiu a segunda nota da Ana, de 7 para 8 |
alunos["Ana"]["email"] = "…" | 2 (a ficha) | Criou um campo novo só na ficha da Ana |
Repare na última: só a Ana ganhou o campo email. As fichas não precisam ter os mesmos campos, mas isso tem um preço: quem ler alunos["Bia"]["email"] leva um KeyError. Para campos que nem todo mundo tem, leia com get (seção 5).
Missão: antes dos print, cadastre o aluno "Caio", de 15 anos, com a lista de notas [10], e lance a nota 9 no fim da lista da Bia.
alunos = {
"Ana": {"idade": 15, "notas": [9, 7, 10]},
"Bia": {"idade": 16, "notas": [6, 8, 7]},
}
# cadastre o Caio e lance a nota da Bia aqui
print(alunos.get("Caio"))
print(alunos["Bia"]["notas"])
alunos = {
"Ana": {"idade": 15, "notas": [9, 7, 10]},
"Bia": {"idade": 16, "notas": [6, 8, 7]},
}
alunos["Caio"] = {"idade": 15, "notas": [10]}
alunos["Bia"]["notas"].append(9)
print(alunos.get("Caio"))
print(alunos["Bia"]["notas"])
3. Percorrendo o cadastro
Com items() (aula Percorrendo um Dicionário), cada volta do for entrega o nome do aluno e a ficha dele. Da ficha você tira o que precisar, como a lista de notas para calcular a média:
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
"Bia": {"idade": 16, "cidade": "Picos", "notas": [6, 8, 7]},
"Caio": {"idade": 15, "cidade": "Parnaíba", "notas": [10, 9]},
}
for nome, dados in alunos.items():
media = sum(dados["notas"]) / len(dados["notas"])
print(f"{nome} ({dados['cidade']}): média {media:.1f}")
$ python3 n_percorrer.py Ana (Teresina): média 8.7 Bia (Picos): média 7.0 Caio (Parnaíba): média 9.5
- for nome, dados in alunos.items():
- A cada volta,
nomerecebe a chave ("Ana") edadosrecebe a ficha inteira (o dicionário de dentro). - dados["notas"]
- Como
dadosjá é a ficha, basta um colchete para chegar às notas. - sum(…) / len(…)
- A soma das notas dividida pela quantidade: a média. O Caio tem só duas notas, e a conta funciona do mesmo jeito.
- {dados['cidade']}
- Dentro da f-string com aspas duplas, a chave vai com aspas simples.
Três níveis de dicionário
Nada impede mais níveis. Uma escola com turmas, cada turma com alunos, cada aluno com notas:
escola = {
"1A": {
"Ana": {"notas": [9, 7, 10]},
"Bia": {"notas": [6, 8, 7]},
},
"1B": {
"Caio": {"notas": [10, 9]},
},
}
print(escola["1A"]["Bia"]["notas"][1])
for turma, alunos in escola.items():
print(f"Turma {turma}: {len(alunos)} aluno(s)")
$ python3 n_tres.py 8 Turma 1A: 2 aluno(s) Turma 1B: 1 aluno(s)
Funciona, mas cada nível a mais é um colchete a mais para lembrar. Dois ou três níveis costumam bastar; se passar disso, é sinal de que vale a pena organizar de outro jeito (as classes, mais adiante no guia, são uma forma).
4. Dicionário de dicionários ou lista de dicionários?
Há outra forma muito comum de guardar um cadastro: uma lista em que cada item é um dicionário. Em vez de o nome ser a chave, ele vira um campo da ficha:
turma = [
{"nome": "Ana", "nota": 9},
{"nome": "Bia", "nota": 6},
{"nome": "Caio", "nota": 10},
]
print(turma[0]["nome"])
for aluno in turma:
print(aluno["nome"], "tirou", aluno["nota"])
turma.append({"nome": "Davi", "nota": 7})
print(len(turma))
$ python3 n_lista_dicts.py Ana Ana tirou 9 Bia tirou 6 Caio tirou 10 4
Aqui turma[0] é uma posição (o primeiro da lista), e só depois vem a chave ["nome"].
| Dicionário de dicionários | Lista de dicionários | |
|---|---|---|
| Forma | {"Ana": {"nota": 9}, …} | [{"nome": "Ana", "nota": 9}, …] |
| Achar a Ana | Direto: alunos["Ana"] | Precisa percorrer a lista procurando |
| Nomes repetidos (duas Anas) | Não pode: a chave é única | Pode |
| Ordem | A de inserção | A da lista (dá para ordenar) |
| Quando usar | Você procura por um nome ou código que não se repete | Você percorre todos, ordena, ou os itens não têm um nome único. É o formato que costuma vir de arquivos JSON e CSV |
5. Mostrando arrumado e lendo com cuidado
O pprint
Com o print comum, um cadastro inteiro sai numa linha só, comprida. O Python tem uma ferramenta pronta para mostrar estruturas aninhadas arrumadas: o pprint ("pretty print", impressão bonita). Ele fica num módulo que você precisa trazer com import (o assunto da aula Módulos e import); por enquanto, basta copiar a primeira linha:
from pprint import pprint
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
"Bia": {"idade": 16, "cidade": "Picos", "notas": [6, 8, 7]},
}
print(alunos)
pprint(alunos, width=40, sort_dicts=False)
$ python3 n_pprint.py
{'Ana': {'idade': 15, 'cidade': 'Teresina', 'notas': [9, 7, 10]}, 'Bia': {'idade': 16, 'cidade': 'Picos', 'notas': [6, 8, 7]}}
{'Ana': {'idade': 15,
'cidade': 'Teresina',
'notas': [9, 7, 10]},
'Bia': {'idade': 16,
'cidade': 'Picos',
'notas': [6, 8, 7]}}
- from pprint import pprint
- "Do módulo
pprint, traga a funçãopprint." - width=40
- A largura máxima de cada linha. Quando não cabe, o
pprintquebra e alinha os níveis. - sort_dicts=False
- Sem isto, o
pprintmostra as chaves em ordem alfabética (cidadeantes deidade), e não na ordem em que você as criou. ComFalse, ele respeita a ordem do dicionário.
get em cada nível
Para um aluno que pode não existir, ou um campo que nem toda ficha tem, use get. E para encadear, dê como padrão um dicionário vazio {}, que aceita o próximo get:
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
}
print(alunos.get("Davi"))
print(alunos.get("Davi", {}).get("cidade"))
print(alunos.get("Ana", {}).get("cidade"))
print(alunos["Ana"].get("email", "sem email"))
$ python3 n_get.py None None Teresina sem email
O Davi não existe: alunos.get("Davi", {}) devolve {}, e {}.get("cidade") devolve None, sem erro nenhum. Sem o {} como padrão, o primeiro get devolveria None e o segundo passo daria erro; veja a seguir.
Formas diferentes de ler um dado lá de dentro
| Forma | Se a chave não existir | Quando usar |
|---|---|---|
alunos["Ana"]["cidade"] | KeyError | Quando você tem certeza de que tudo existe. É a forma mais curta |
ficha = alunos["Ana"]ficha["cidade"] | KeyError | Quando vai usar a mesma ficha várias vezes, ou para achar em qual nível está o erro |
alunos.get("Ana", {}).get("cidade") | None, sem erro | Quando o aluno ou o campo podem faltar |
Quando dá errado
Com vários níveis, a pergunta mais importante diante de um erro é: em qual colchete ele aconteceu? Os sinais ~~~^^^ que o Python mostra embaixo da linha respondem: os ^ ficam embaixo do colchete que falhou.
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
}
print(alunos["Ana"]["nota"])
$ python3 n_erro_chave.py
Traceback (most recent call last):
File "/home/ana/n_erro_chave.py", line 4, in <module>
print(alunos["Ana"]["nota"])
~~~~~~~~~~~~~^^^^^^^^
KeyError: 'nota'
- ~~~~~~~~~~~~~^^^^^^^^
- Os tils cobrem
alunos["Ana"](essa parte deu certo) e os^ficam embaixo de["nota"]: foi no segundo nível, dentro da ficha da Ana. - KeyError: 'nota'
- A ficha não tem o campo
'nota'. Ela tem'notas', no plural.
Como consertar: alunos["Ana"]["notas"]. Na dúvida sobre os nomes dos campos, mostre a ficha: print(alunos["Ana"].keys()).
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
}
print(alunos["cidade"]["Ana"])
$ python3 n_erro_ordem.py
Traceback (most recent call last):
File "/home/ana/n_erro_ordem.py", line 4, in <module>
print(alunos["cidade"]["Ana"])
~~~~~~^^^^^^^^^^
KeyError: 'cidade'
Agora os ^ estão embaixo do primeiro colchete. No nível de fora, as chaves são os nomes dos alunos; 'cidade' é uma chave do nível de dentro. Como consertar: do mais de fora para o mais de dentro: primeiro o aluno, depois o campo: alunos["Ana"]["cidade"].
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
}
print(alunos["Ana"]["notas"]["primeira"])
$ python3 n_erro_lista.py
Traceback (most recent call last):
File "/home/ana/n_erro_lista.py", line 4, in <module>
print(alunos["Ana"]["notas"]["primeira"])
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
TypeError: list indices must be integers or slices, not str
"Índices de lista precisam ser inteiros ou fatias, não str". O terceiro colchete já está numa lista (as notas), e lista não tem chave, tem posição. Como consertar: alunos["Ana"]["notas"][0]. Saber o tipo de cada nível evita este erro; a tabela do começo da aula ajuda.
get que devolveu None, e depois um colchete
alunos = {
"Ana": {"idade": 15, "cidade": "Teresina", "notas": [9, 7, 10]},
}
print(alunos.get("Davi")["cidade"])
$ python3 n_erro_none.py
Traceback (most recent call last):
File "/home/ana/n_erro_none.py", line 4, in <module>
print(alunos.get("Davi")["cidade"])
~~~~~~~~~~~~~~~~~~^^^^^^^^^^
TypeError: 'NoneType' object is not subscriptable
O Davi não existe, então alunos.get("Davi") devolveu None. E None["cidade"] não faz sentido: "um objeto NoneType (o tipo do None) não aceita colchetes". O get evitou o erro no primeiro nível, mas ele apareceu no segundo. Como consertar: dê um dicionário vazio como padrão (alunos.get("Davi", {}).get("cidade")) ou pergunte antes: if "Davi" in alunos:.
Para não repetir, a Ana criou uma ficha "modelo" e usou para os dois alunos novos:
modelo = {"idade": 15, "notas": []}
alunos = {}
alunos["Ana"] = modelo
alunos["Bia"] = modelo
alunos["Ana"]["notas"].append(9)
print(alunos)
$ python3 n_compartilhado.py
{'Ana': {'idade': 15, 'notas': [9]}, 'Bia': {'idade': 15, 'notas': [9]}}
Ela lançou a nota só para a Ana, e a Bia também ganhou um 9! alunos["Ana"] = modelo não copiou a ficha: guardou a mesma ficha nas duas gavetas. É uma ficha só, com dois nomes na etiqueta; mexeu por uma gaveta, aparece pela outra. Como consertar: crie uma ficha nova para cada aluno, por exemplo dentro de um for:
alunos = {}
for nome in ["Ana", "Bia"]:
alunos[nome] = {"idade": 15, "notas": []}
alunos["Ana"]["notas"].append(9)
print(alunos)
$ python3 n_compartilhado_ok.py
{'Ana': {'idade': 15, 'notas': [9]}, 'Bia': {'idade': 15, 'notas': []}}
A cada volta, {"idade": 15, "notas": []} cria um dicionário e uma lista novos. Por que isso acontece, e por que até modelo.copy() não resolveria (a lista de notas continuaria dividida), é o assunto da aula Cópia Rasa e Cópia Profunda.
Referência rápida
| Escrita | O que faz |
|---|---|
d["a"]["b"] | Do dicionário d, a chave "a"; dela, a chave "b" |
d["a"]["lista"][0] | Chave, chave e, no fim, a posição de uma lista |
d["novo"] = {…} | Acrescenta um item com o seu próprio dicionário |
d["a"]["b"] = v | Cria ou troca um campo lá de dentro |
d["a"]["lista"].append(x) | Acrescenta numa lista lá de dentro |
for chave, dentro in d.items(): | Percorre o nível de fora; dentro é o dicionário de cada um |
d.get("a", {}).get("b") | Lê dois níveis sem erro (devolve None se faltar) |
pprint(d, sort_dicts=False) | Mostra arrumado, na ordem do dicionário (precisa de from pprint import pprint) |
| Mensagem | Causa provável |
|---|---|
KeyError: 'nota' | Nome de chave errado no nível marcado pelos ^ |
KeyError: 'cidade' logo no primeiro colchete | Níveis na ordem trocada |
TypeError: list indices must be integers or slices, not str | Chave de texto num nível que é lista |
TypeError: 'NoneType' object is not subscriptable | Colchete depois de um get que devolveu None |
| Mudou um aluno e o outro mudou junto | Os dois apontam para o mesmo dicionário |
Exercícios
1. A idade da Bia
Complete para ler a idade da Bia no cadastro alunos:
2. A última nota
Complete o índice para ler a última nota da Ana:
3. Lançando uma nota
Complete o método que põe a nota 8 no fim da lista do Caio:
4. Em alunos["Ana"]["notas"][0], o que são os três valores entre colchetes?
5. alunos["Ana"]["notas"]["primeira"] dá qual erro?
6. Você precisa achar rapidamente a ficha de um aluno pelo número de matrícula, que nunca se repete. O que é mais adequado?
Missão final: o boletim da turma
Missão: percorra o cadastro e, para cada aluno, calcule a média das notas. Mostre uma linha como Ana: 8.7 (passou), com uma casa decimal; quem tiver média menor que 7 fica de (recuperação).
alunos = {
"Ana": {"idade": 15, "notas": [9, 7, 10]},
"Bia": {"idade": 16, "notas": [6, 5, 7]},
"Caio": {"idade": 15, "notas": [10, 9]},
}
for nome, dados in alunos.items():
print(nome)
alunos = {
"Ana": {"idade": 15, "notas": [9, 7, 10]},
"Bia": {"idade": 16, "notas": [6, 5, 7]},
"Caio": {"idade": 15, "notas": [10, 9]},
}
for nome, dados in alunos.items():
media = sum(dados["notas"]) / len(dados["notas"])
if media >= 7:
situacao = "passou"
else:
situacao = "recuperação"
print(f"{nome}: {media:.1f} ({situacao})")
Resumo
- Um dicionário pode guardar outros dicionários e listas: é um dicionário aninhado, como um fichário com fichas e campos.
- Para ler lá de dentro, um colchete por nível, da esquerda para a direita:
alunos["Ana"]["notas"][0]. Chave onde é dicionário, posição onde é lista. - Para mudar, chegue ao nível certo e use a operação de sempre:
= valor,.append(),del. for nome, dados in alunos.items():entrega cada ficha inteira emdados.- Dicionário de dicionários para achar por uma chave única; lista de dicionários para percorrer e ordenar.
pprint(d, sort_dicts=False)mostra arrumado;d.get("a", {}).get("b")lê sem erro.- Num erro, os
^mostram qual colchete falhou. E cuidado: usar o mesmo dicionário para dois itens faz os dois mudarem juntos.