Las aplicaciones suelen combinar varias fuentes de configuración: argumentos de línea de comandos, variables de entorno, un archivo local y valores predeterminados. Copiar todo en un nuevo diccionario funciona, pero pierde el origen de cada valor y debe repetirse cuando una capa cambia. collections.ChainMap presenta varios mappings como una sola vista y los consulta desde la primera capa hasta la última.
Esta guía explica precedencia, lectura y escritura, la lista maps, new_child(), parents, scopes anidados, configuración, diferencias con la unión de diccionarios, rendimiento, mutabilidad, serialización y diseño seguro.
Primer ChainMap
from collections import ChainMap
predeterminados = {"tema": "claro", "timeout": 30}
usuario = {"tema": "oscuro"}
config = ChainMap(usuario, predeterminados)
print(config["tema"]) # oscuro
print(config["timeout"]) # 30
La búsqueda recorre los mappings en el orden suministrado. La primera aparición gana. Los diccionarios originales no se copian.
Una vista dinámica
predeterminados["timeout"] = 60
print(config["timeout"]) # 60
ChainMap conserva referencias, por lo que las modificaciones de cualquier capa aparecen inmediatamente. Esto es útil para configuración viva, pero sorprende cuando el consumidor esperaba un snapshot inmutable.
Las escrituras van al primer mapa
config["timeout"] = 10
print(usuario)
# {'tema': 'oscuro', 'timeout': 10}
Asignaciones, eliminaciones, pop() y otras operaciones mutables afectan solamente al primer mapping. Aunque una clave exista más abajo, la asignación crea una sobreposición en la primera capa.
Eliminar una clave
del config["tema"]
La eliminación quita la clave solo del primer mapa. Si existe en una capa posterior, ese valor vuelve a ser visible. Si no existe en el primer mapping, se genera KeyError aunque aparezca más abajo.
La lista maps
print(config.maps)
maps es la lista real de mappings. Puedes inspeccionar, añadir o reordenar capas, pero los cambios estructurales modifican la precedencia para todos los consumidores que comparten la instancia.
Configuración en cuatro niveles
config = ChainMap(
argumentos_cli,
valores_entorno,
archivo_config,
predeterminados,
)
Coloca primero la fuente de mayor prioridad. Convierte tipos antes de montar la cadena: el entorno contiene strings, mientras que los defaults pueden ser enteros, booleanos, rutas u objetos.
Diferencia con la unión de diccionarios
mezclado = predeterminados | archivo_config | valores_entorno | argumentos_cli
El operador | crea un nuevo diccionario resuelto en ese momento. ChainMap crea una vista dinámica, conserva las capas y evita copiar todas las entradas. Un dict materializado es más simple para JSON, aislamiento y ejecución reproducible.
Materializar un snapshot
snapshot = dict(config)
Convierte a dict cuando necesites estado estable, comparación, serialización o protección frente a cambios posteriores. La conversión es superficial: valores mutables internos continúan compartidos.
new_child
scope_global = ChainMap(globales)
scope_funcion = scope_global.new_child(locales)
new_child() crea otro ChainMap con un nuevo primer mapping seguido de las capas existentes. Si no se proporciona uno, crea un diccionario vacío. Es útil para scopes léxicos, overrides temporales y contextos anidados.
parents
scope_padre = scope_funcion.parents
parents devuelve una nueva vista excluyendo el primer mapa. No modifica la cadena original ni destruye datos.
Modelar scopes
globales = {"impuesto": 0.1}
locales = {"subtotal": 100}
scope = ChainMap(locales, globales)
La consulta busca nombres locales antes que globales. La asignación escribe localmente y modela shadowing. Intérpretes y motores de templates pueden usar este patrón.
Contexto temporal
base = ChainMap(config_global)
temporal = base.new_child({"debug": True})
ejecutar(temporal)
No es necesario copiar la base. Al descartar la vista temporal, deja de usarse el override. Las mutaciones directas de mapas profundos compartidos siguen siendo visibles.
Orden de iteración
La iteración produce cada clave visible una vez. Su orden se parece a actualizar un diccionario desde el último mapping hacia el primero, mientras la búsqueda de valores recorre del primero al último. No confundas orden de presentación con precedencia.
Longitud y pertenencia
len(config) cuenta claves únicas visibles, no la suma de todos los tamaños. La operación in puede recorrer varias capas.
def indice_origen(chain, clave):
for indice, mapa in enumerate(chain.maps):
if clave in mapa:
return indice
return None
Este helper identifica la capa de mayor prioridad que define una clave.
Actualizar una capa específica
Para modificar un mapping profundo, accede directamente:
config.maps[2]["timeout"] = 45
Encapsula posiciones numéricas en helpers o nombres para evitar fragilidad cuando cambie el orden.
Semántica DeepChainMap
Algunas aplicaciones desean actualizar la primera capa donde ya existe la clave:
class DeepChainMap(ChainMap):
def __setitem__(self, clave, valor):
for mapa in self.maps:
if clave in mapa:
mapa[clave] = valor
return
self.maps[0][clave] = valor
Este comportamiento es distinto al contrato estándar. Documéntalo claramente.
Capas de solo lectura
MappingProxyType puede proteger mappings profundos. El primer mapa debe seguir siendo mutable si los consumidores usan operaciones de escritura de ChainMap.
Validación y schemas
ChainMap no valida nombres ni convierte valores. Parsear cada fuente, validar el resultado resuelto y decidir si se permiten claves desconocidas sigue siendo responsabilidad de la aplicación.
None como override
Una primera capa con {"timeout": None} oculta el valor inferior. Decide si None significa desactivado, nulo explícito o “usar predeterminado”. Filtra el valor antes si debería significar ausencia.
Concurrencia
ChainMap y los diccionarios subyacentes no sincronizan accesos. Los lectores pueden observar estados intermedios mientras otra thread modifica capas. Prefiere snapshots inmutables o locks para configuración concurrente.
Serialización
Los encoders JSON no suelen conocer ChainMap. Usa dict(chain) para valores resueltos o serializa chain.maps para conservar fuentes y precedencia. Elimina secretos antes de registrar datos.
Rendimiento
Una clave del primer mapa se encuentra rápido; una clave solo en la última capa requiere comprobar todas las anteriores. Unas pocas capas son económicas. Cientos de mappings en rutas calientes deberían aplanarse o rediseñarse.
ChainMap frente a defaultdict
defaultdict crea valores ausentes en un único diccionario. ChainMap busca en varios mappings existentes. Resuelven problemas diferentes y pueden combinarse, aunque la creación automática en la primera capa puede ocultar valores inferiores.
ChainMap frente a contextvars
ChainMap modela precedencia de mappings. contextvars propaga estado local por tarea asíncrona. No uses una cadena global mutable como sustituto de contexto por tarea.
Errores comunes
- Esperar una copia: las capas son referencias vivas.
- Esperar que la escritura actualice la fuente original: va al primer mapa.
- Eliminar una clave profunda: del actúa solo en el primer mapa.
- Ignorar tipos de las fuentes: las variables de entorno son strings.
- Confundir iteración con precedencia: no siguen el mismo orden conceptual.
- Compartir mutaciones sin sincronización: pueden observarse estados incoherentes.
Ejemplo completo: configuración de aplicación
from collections import ChainMap
PREDETERMINADOS = {
"host": "127.0.0.1",
"puerto": 8000,
"debug": False,
}
def crear_config(cli, entorno, archivo):
entorno_parseado = {}
if "APP_PUERTO" in entorno:
entorno_parseado["puerto"] = int(entorno["APP_PUERTO"])
if "APP_DEBUG" in entorno:
entorno_parseado["debug"] = entorno["APP_DEBUG"].lower() == "true"
capas = ChainMap(cli, entorno_parseado, archivo, PREDETERMINADOS)
resuelta = dict(capas)
if not 1 <= resuelta["puerto"] <= 65535:
raise ValueError("puerto inválido")
return resuelta
La función usa ChainMap para precedencia y devuelve un snapshot validado para una ejecución estable.
Conclusión
collections.ChainMap ofrece una vista ligera sobre mappings en capas, preservando precedencia y origen sin copiar cada entrada. Es ideal para configuración, scopes y overrides temporales.
La documentación oficial de ChainMap define la API. Ordena las capas conscientemente, materializa snapshots cuando necesites aislamiento y recuerda que la escritura afecta solo al primer mapping.







