Dicionários aninhados e estruturas complexas

1. Fundamentos dos Dicionários Aninhados

Dicionários aninhados são dicionários que contêm outros dicionários como valores, formando uma estrutura hierárquica de dados. Essa técnica é fundamental em Python para representar informações com múltiplos níveis de profundidade, como dados JSON, configurações de sistemas e estruturas de dados hierárquicas.

# Sintaxe básica de um dicionário aninhado
usuario = {
    "nome": "Ana Silva",
    "contato": {
        "email": "ana@exemplo.com",
        "telefone": {
            "residencial": "11-3333-4444",
            "celular": "11-99999-8888"
        }
    },
    "endereco": {
        "rua": "Av. Paulista",
        "numero": 1000,
        "cidade": "São Paulo"
    }
}

# Acessando valores aninhados
print(usuario["contato"]["email"])  # ana@exemplo.com
print(usuario["contato"]["telefone"]["celular"])  # 11-99999-8888

Casos de uso comuns incluem representação de dados de APIs REST, configurações de aplicações, estruturas de árvore e dados geográficos.

2. Construção e Manipulação de Estruturas Aninhadas

Podemos construir dicionários aninhados manualmente ou dinamicamente com loops.

# Construção manual
config = {
    "banco": {
        "host": "localhost",
        "porta": 5432,
        "credenciais": {
            "usuario": "admin",
            "senha": "secret"
        }
    }
}

# Construção com loop
alunos = {}
nomes = ["João", "Maria", "Pedro"]
notas = [[8, 7, 9], [10, 9, 8], [6, 7, 8]]

for i, nome in enumerate(nomes):
    alunos[nome] = {
        "matricula": i + 1,
        "notas": notas[i],
        "media": sum(notas[i]) / len(notas[i])
    }

# Adicionando novo nível dinamicamente
alunos["João"]["endereco"] = {"cidade": "Rio", "estado": "RJ"}

# Removendo nível
del alunos["João"]["endereco"]["estado"]

# Atualizando valores em profundidade
alunos["Maria"]["notas"].append(9)
alunos["Maria"]["media"] = sum(alunos["Maria"]["notas"]) / len(alunos["Maria"]["notas"])

# Usando update() para mesclar
novo_dado = {"João": {"status": "ativo"}}
alunos.update(novo_dado)

3. Navegação e Acesso a Dados Profundos

O acesso encadeado é a forma mais direta, mas requer cuidado com chaves inexistentes.

dados_empresa = {
    "departamentos": {
        "TI": {
            "gerente": "Carlos",
            "funcionarios": ["Ana", "Bob", "Carol"],
            "orcamento": 500000
        },
        "RH": {
            "gerente": "Diana",
            "funcionarios": ["Eve", "Frank"],
            "orcamento": 200000
        }
    }
}

# Acesso encadeado
print(dados_empresa["departamentos"]["TI"]["gerente"])  # Carlos

# Tratamento com try/except
try:
    orcamento = dados_empresa["departamentos"]["Marketing"]["orcamento"]
except KeyError:
    orcamento = 0
    print("Departamento não encontrado")

# Uso de get() para navegação segura
orcamento_marketing = dados_empresa.get("departamentos", {}).get("Marketing", {}).get("orcamento", 0)
print(f"Orçamento Marketing: {orcamento_marketing}")  # 0

# Função auxiliar para acesso profundo
def acessar_aninhado(dicionario, *chaves, padrao=None):
    resultado = dicionario
    for chave in chaves:
        try:
            resultado = resultado[chave]
        except (KeyError, TypeError):
            return padrao
    return resultado

print(acessar_aninhado(dados_empresa, "departamentos", "TI", "orcamento"))  # 500000
print(acessar_aninhado(dados_empresa, "departamentos", "Vendas", "orcamento", padrao=0))  # 0

4. Iteração em Estruturas Aninhadas

Para percorrer estruturas aninhadas, precisamos de loops aninhados ou recursão.

# Iteração em dois níveis
catalogo = {
    "eletronicos": {
        "smartphones": 10,
        "laptops": 5,
        "tablets": 3
    },
    "roupas": {
        "camisetas": 20,
        "calcas": 15,
        "sapatos": 8
    }
}

for categoria, itens in catalogo.items():
    print(f"\nCategoria: {categoria}")
    for item, quantidade in itens.items():
        print(f"  {item}: {quantidade} unidades")

# Iteração recursiva para profundidade arbitrária
def iterar_aninhado(dicionario, prefixo=""):
    for chave, valor in dicionario.items():
        caminho = f"{prefixo}.{chave}" if prefixo else chave
        if isinstance(valor, dict):
            iterar_aninhado(valor, caminho)
        else:
            print(f"{caminho}: {valor}")

# Função geradora para extrair todos os pares chave-valor
def extrair_pares(dicionario, prefixo=""):
    for chave, valor in dicionario.items():
        caminho = f"{prefixo}.{chave}" if prefixo else chave
        if isinstance(valor, dict):
            yield from extrair_pares(valor, caminho)
        else:
            yield (caminho, valor)

print("\n--- Iteração recursiva ---")
iterar_aninhado(catalogo)

print("\n--- Pares extraídos ---")
for caminho, valor in extrair_pares(catalogo):
    print(f"{caminho} = {valor}")

5. Combinação com Listas e Outras Coleções

Estruturas complexas frequentemente combinam dicionários com listas e outras coleções.

# Lista de dicionários (estrutura tabular)
funcionarios = [
    {"id": 1, "nome": "Ana", "cargo": "Analista", "salario": 5000},
    {"id": 2, "nome": "Bob", "cargo": "Programador", "salario": 6000},
    {"id": 3, "nome": "Carol", "cargo": "Gerente", "salario": 8000}
]

# Dicionário com listas como valores (agrupamento)
por_cargo = {}
for func in funcionarios:
    cargo = func["cargo"]
    if cargo not in por_cargo:
        por_cargo[cargo] = []
    por_cargo[cargo].append(func["nome"])

print("Funcionários por cargo:", por_cargo)

# Misturando tuplas, sets e dicionários
dados_complexos = {
    "projeto": "Sistema X",
    "equipe": {
        "desenvolvedores": [("Ana", "Python"), ("Bob", "Java")],
        "testadores": {"Carlos", "Diana"}  # set
    },
    "versoes": {
        1.0: {"data": "2024-01", "features": ["login", "cadastro"]},
        2.0: {"data": "2024-06", "features": ["relatorios", "dashboard"]}
    }
}

# Acessando estruturas mistas
print(dados_complexos["equipe"]["desenvolvedores"][0])  # ('Ana', 'Python')
print(dados_complexos["versoes"][2.0]["features"])  # ['relatorios', 'dashboard']

6. Técnicas Avançadas de Manipulação

Técnicas como mesclagem profunda, achatamento e transformação são essenciais para manipular estruturas complexas.

from copy import deepcopy

# Mesclagem profunda (deep merge)
def deep_merge(dict1, dict2):
    resultado = deepcopy(dict1)
    for chave, valor in dict2.items():
        if chave in resultado and isinstance(resultado[chave], dict) and isinstance(valor, dict):
            resultado[chave] = deep_merge(resultado[chave], valor)
        else:
            resultado[chave] = deepcopy(valor)
    return resultado

config_padrao = {"banco": {"host": "localhost", "porta": 5432}}
config_usuario = {"banco": {"host": "192.168.1.100"}, "debug": True}
config_final = deep_merge(config_padrao, config_usuario)
print("Config mesclada:", config_final)

# Achatamento (flatten) de dicionário aninhado
def flatten_dict(dicionario, separador="_", prefixo=""):
    itens = []
    for chave, valor in dicionario.items():
        novo_prefixo = f"{prefixo}{separador}{chave}" if prefixo else chave
        if isinstance(valor, dict):
            itens.extend(flatten_dict(valor, separador, novo_prefixo).items())
        else:
            itens.append((novo_prefixo, valor))
    return dict(itens)

dados_aninhados = {"a": {"b": {"c": 1, "d": 2}, "e": 3}}
dados_planos = flatten_dict(dados_aninhados)
print("Dados achatados:", dados_planos)  # {'a_b_c': 1, 'a_b_d': 2, 'a_e': 3}

# Transformação com compreensão de dicionários
quadrados = {x: x**2 for x in range(5)}
print("Quadrados:", quadrados)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Transformação aninhada com compreensão
matriz = {(i, j): i * j for i in range(3) for j in range(3)}
print("Matriz:", matriz)

7. Padrões e Boas Práticas

Ao trabalhar com dicionários aninhados, algumas práticas ajudam a manter o código limpo e eficiente.

# Quando usar dicionários aninhados vs. classes
# Use dicionários para dados simples e temporários
# Use classes para comportamentos e validações

from dataclasses import dataclass

@dataclass
class Endereco:
    rua: str
    cidade: str
    cep: str

@dataclass
class Usuario:
    nome: str
    endereco: Endereco

# Alternativa com dicionário (mais flexível, menos seguro)
usuario_dict = {
    "nome": "Ana",
    "endereco": {"rua": "Av. Brasil", "cidade": "SP", "cep": "01001-000"}
}

# Evitando aninhamento excessivo
# Ruim: config["database"]["connection"]["pool"]["size"]
# Melhor: criar objetos separados para cada nível

# Serialização com JSON
import json

dados = {"nome": "João", "idade": 30, "endereco": {"cidade": "RJ"}}
with open("dados.json", "w") as f:
    json.dump(dados, f, indent=2)

with open("dados.json", "r") as f:
    dados_carregados = json.load(f)

print("Dados carregados do JSON:", dados_carregados)

Referências