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.

Você vai aprender
  • 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ívelNo fichárioEm PythonTipo
1O fichário inteiroalunosDicionário (chave: nome do aluno)
2A ficha da Anaalunos["Ana"]Dicionário (chave: nome do campo)
3O campo notasalunos["Ana"]["notas"]Lista
4A primeira notaalunos["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]}
LinhaNívelO 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"] = 172 (a ficha)Trocou o valor de um campo que já existia
alunos["Ana"]["notas"][1] = 84 (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, nome recebe a chave ("Ana") e dados recebe a ficha inteira (o dicionário de dentro).
dados["notas"]
Como dados já é 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áriosLista de dicionários
Forma{"Ana": {"nota": 9}, …}[{"nome": "Ana", "nota": 9}, …]
Achar a AnaDireto: alunos["Ana"]Precisa percorrer a lista procurando
Nomes repetidos (duas Anas)Não pode: a chave é únicaPode
OrdemA de inserçãoA da lista (dá para ordenar)
Quando usarVocê procura por um nome ou código que não se repeteVocê 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ção pprint."
width=40
A largura máxima de cada linha. Quando não cabe, o pprint quebra e alinha os níveis.
sort_dicts=False
Sem isto, o pprint mostra as chaves em ordem alfabética (cidade antes de idade), e não na ordem em que você as criou. Com False, 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

FormaSe a chave não existirQuando usar
alunos["Ana"]["cidade"]KeyErrorQuando você tem certeza de que tudo existe. É a forma mais curta
ficha = alunos["Ana"]
ficha["cidade"]
KeyErrorQuando 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 erroQuando 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.

Campo com o nome errado
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()).

Os níveis na ordem trocada
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"].

Usar uma chave numa lista
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:.

Dois alunos com a mesma ficha: sem erro, resultado errado

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

EscritaO 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"] = vCria 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)
MensagemCausa provável
KeyError: 'nota'Nome de chave errado no nível marcado pelos ^
KeyError: 'cidade' logo no primeiro colcheteNíveis na ordem trocada
TypeError: list indices must be integers or slices, not strChave de texto num nível que é lista
TypeError: 'NoneType' object is not subscriptableColchete depois de um get que devolveu None
Mudou um aluno e o outro mudou juntoOs dois apontam para o mesmo dicionário

Exercícios

1. A idade da Bia

Complete para ler a idade da Bia no cadastro alunos:

print(alunos["Bia"])

2. A última nota

Complete o índice para ler a última nota da Ana:

alunos["Ana"]["notas"][]

3. Lançando uma nota

Complete o método que põe a nota 8 no fim da lista do Caio:

alunos["Caio"]["notas"].(8)

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 em dados.
  • 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.