Aplicações frequentemente combinam várias fontes de configuração: argumentos da linha de comando, variáveis de ambiente, arquivo local e valores padrão. Copiar todos esses dicionários para um novo objeto funciona, mas perde a origem de cada valor e exige reconstrução quando uma camada muda. collections.ChainMap apresenta vários mappings como uma única visão, pesquisando da primeira camada para a última.
Neste guia, você aprenderá precedência, leitura e escrita, maps, new_child(), parents, escopos aninhados, configurações, diferenças para | e update(), desempenho, mutabilidade e práticas seguras.
Primeiro ChainMap
from collections import ChainMap
padroes = {"tema": "claro", "timeout": 30}
usuario = {"tema": "escuro"}
config = ChainMap(usuario, padroes)
print(config["tema"]) # escuro
print(config["timeout"]) # 30
A busca percorre os mapas na ordem fornecida. A primeira ocorrência vence. Os dicionários originais não são copiados.
Uma visão dinâmica
padroes["timeout"] = 60
print(config["timeout"]) # 60
Como ChainMap guarda referências, alterações nas camadas aparecem imediatamente. Isso é útil para configurações vivas, mas pode surpreender quando o consumidor esperava um snapshot imutável.
Escritas vão para o primeiro mapa
config["timeout"] = 10
print(usuario)
# {'tema': 'escuro', 'timeout': 10}
Atribuições, exclusões, pop() e operações semelhantes afetam apenas o primeiro mapping. Mesmo que a chave exista em uma camada posterior, a atribuição cria uma sobreposição no primeiro mapa.
Excluir uma chave
del config["tema"]
A exclusão remove a chave somente do primeiro mapa. Se a mesma chave existir em uma camada posterior, ela volta a ficar visível. Se a chave não existir no primeiro mapping, a operação gera KeyError, mesmo que exista mais abaixo.
A lista maps
print(config.maps)
maps é a lista real de mappings. Você pode inspecionar, adicionar ou reordenar camadas, mas alterações estruturais devem ser feitas com cuidado porque mudam a precedência para todos os consumidores da mesma instância.
Configuração em quatro níveis
config = ChainMap(
argumentos_cli,
variaveis_ambiente,
arquivo_config,
padroes,
)
Coloque a fonte de maior prioridade primeiro. Normalize tipos antes de montar a cadeia: variáveis de ambiente são strings, enquanto defaults podem ser inteiros ou booleanos.
Diferença para mesclar dicionários
mesclado = padroes | arquivo_config | variaveis_ambiente | argumentos_cli
O operador | cria um novo dicionário com valores resolvidos naquele momento. ChainMap cria uma visão dinâmica, preserva as camadas e evita copiar todas as entradas. O dicionário materializado é mais simples para serialização, hashing conceitual e isolamento.
Quando materializar
snapshot = dict(config)
Converta para dict quando precisar de um snapshot, enviar JSON, comparar estados ou impedir que mudanças posteriores alterem o resultado. Lembre que a conversão é rasa: objetos mutáveis internos continuam compartilhados.
new_child
escopo_global = ChainMap(globais)
escopo_funcao = escopo_global.new_child(locais)
new_child() cria outro ChainMap com um novo primeiro mapping e as camadas antigas depois. Se nenhum mapa for fornecido, um dicionário vazio é criado. Esse padrão modela escopos, overrides temporários e contextos aninhados.
parents
escopo_pai = escopo_funcao.parents
parents devolve uma nova visão sem o primeiro mapa, equivalente às camadas restantes. Ele não remove nem destrói dados da cadeia original.
Modelando escopos de linguagem
globais = {"taxa": 0.1}
locais = {"subtotal": 100}
escopo = ChainMap(locais, globais)
A consulta encontra primeiro variáveis locais e depois globais. Uma atribuição cria ou atualiza o nome local, simulando shadowing. Interpretadores e engines de template podem usar essa ideia.
Contexto temporário
base = ChainMap(config_global)
temporario = base.new_child({"debug": True})
executar(temporario)
O contexto base não precisa ser copiado. Ao descartar temporario, a sobreposição deixa de ser usada. Entretanto, alterações diretas em mapas compartilhados continuam visíveis.
Iteração e ordem
Iterar sobre ChainMap produz cada chave uma vez. A ordem de iteração corresponde conceitualmente a uma atualização começando pelo último mapa e avançando para o primeiro, enquanto a busca de valores segue do primeiro para o último. Não confunda ordem de exibição com precedência.
Comprimento
len(config) conta chaves únicas visíveis, não a soma dos tamanhos das camadas. Calcular comprimento e iterar pode exigir visitar vários mappings.
Membership
if "timeout" in config:
...
A busca pode percorrer camadas até encontrar a chave. Para saber onde ela foi definida, examine config.maps:
def origem(chain, chave):
for indice, mapa in enumerate(chain.maps):
if chave in mapa:
return indice
return None
Atualizar uma camada específica
ChainMap escreve apenas no primeiro mapa. Para alterar uma camada posterior, acesse-a explicitamente:
config.maps[2]["timeout"] = 45
Encapsule índices em nomes ou objetos para evitar código frágil quando a ordem mudar.
DeepChainMap
Alguns casos desejam atualizar a primeira camada onde a chave já existe. É possível criar uma subclasse:
class DeepChainMap(ChainMap):
def __setitem__(self, chave, valor):
for mapa in self.maps:
if chave in mapa:
mapa[chave] = valor
return
self.maps[0][chave] = valor
Essa semântica é diferente da classe padrão. Documente claramente para não surpreender consumidores.
Maps somente leitura
Você pode incluir MappingProxyType em camadas que não devem ser alteradas. Ainda assim, o primeiro mapa precisa aceitar escrita se você usar operações mutáveis do ChainMap.
Tipos e validação
ChainMap não valida nomes nem converte valores. Para configuração robusta, defina schema, parseie cada fonte e valide o resultado final. O fato de uma chave existir não garante que seu tipo seja correto.
Valores None
Uma camada com {"timeout": None} oculta o valor posterior. Decida se None significa “desativado”, “ausente” ou “usar padrão”. Se deveria significar ausência, filtre a entrada antes de montar a cadeia.
Concorrência
ChainMap e os dicionários subjacentes não oferecem sincronização. Leituras enquanto outra thread altera camadas podem observar estados intermediários. Prefira snapshots imutáveis ou locks quando a configuração muda concorrentemente.
Serialização
Serializadores JSON não conhecem ChainMap diretamente. Use dict(chain) para valores resolvidos ou serialize chain.maps quando precisar preservar fontes e precedência. Nunca exponha segredos de variáveis de ambiente em logs.
Desempenho
A consulta no primeiro mapa é rápida; uma chave presente apenas no último exige verificar todas as camadas. Com poucas configurações, o custo é pequeno. Com centenas de camadas e acesso frequente, materialize um snapshot ou reorganize a arquitetura.
Comparação com defaultdict
defaultdict cria valores ausentes em um único dicionário. ChainMap procura em vários mappings existentes. Eles resolvem problemas diferentes e podem ser combinados, embora efeitos de criação automática devam ser considerados.
Comparação com contextvars
ChainMap modela precedência de mappings. contextvars propaga estado por contexto assíncrono. Não use uma cadeia global mutável como substituto de contexto por tarefa.
Erros comuns
- Esperar cópia: as camadas são referências vivas.
- Achar que escrita atualiza a origem: ela vai para o primeiro mapa.
- Excluir uma chave posterior: del atua apenas no primeiro mapa.
- Ignorar tipos das fontes: ambiente geralmente fornece strings.
- Confiar na ordem de iteração como precedência: são conceitos diferentes.
- Compartilhar mutações sem sincronização: concorrência pode produzir estados incoerentes.
Exemplo completo: configuração de aplicação
from collections import ChainMap
PADROES = {
"host": "127.0.0.1",
"porta": 8000,
"debug": False,
}
def criar_config(cli, ambiente, arquivo):
ambiente_parseado = {}
if "APP_PORTA" in ambiente:
ambiente_parseado["porta"] = int(ambiente["APP_PORTA"])
if "APP_DEBUG" in ambiente:
ambiente_parseado["debug"] = ambiente["APP_DEBUG"].lower() == "true"
camadas = ChainMap(cli, ambiente_parseado, arquivo, PADROES)
resolvida = dict(camadas)
if not 1 <= resolvida["porta"] <= 65535:
raise ValueError("porta inválida")
return resolvida
O exemplo usa ChainMap para precedência, mas retorna um snapshot validado para execução estável.
Conclusão
collections.ChainMap oferece uma visão leve de mappings em camadas, preservando precedência e origem sem copiar todas as entradas. Ele é ideal para configurações, escopos e overrides temporários.
A documentação oficial de ChainMap descreve a API. Use a ordem de camadas conscientemente, materialize snapshots quando precisar de isolamento e lembre que operações de escrita afetam somente o primeiro mapa.







