collections.ChainMap 详解
ChainMap 是 Python 标准库 collections 模块中的一个类,Python 3.3+ 可用。它用于将多个映射(通常是字典)组合成一个单一的、可更新的视图。查找时按顺序从第一个映射到最后一个映射,写入时只作用于第一个映射。
python
from collections import ChainMap
1. 核心思想
ChainMap 不合并字典,而是维护一个映射列表 maps,按优先级从高到低排列。
python
defaults = {'theme': 'dark', 'language': 'en'}
user = {'theme': 'light'}
env = {'language': 'zh'}
config = ChainMap(env, user, defaults)
此时 config.maps 为:
bash
[env, user, defaults]
# 即 [{'language': 'zh'}, {'theme': 'light'}, {'theme': 'dark', 'language': 'en'}]
查找顺序:env → user → defaults。
写入顺序:只写入 env,即 maps[0]。
2. 基本行为
2.1 查找
python
print(config['theme']) # light,来自 user
print(config['language']) # zh,来自 env
print(config['show']) # KeyError
__getitem__ 会依次查找每个映射,返回第一个匹配的值。
2.2 写入
python
config['theme'] = 'blue'
print(env) # {'language': 'zh', 'theme': 'blue'}
print(user) # {'theme': 'light'},不变
print(defaults)# {'theme': 'dark', 'language': 'en'},不变
写入只影响第一个映射 maps[0]。
2.3 删除
python
del config['language']
print(env) # {'theme': 'blue'}
print(config['language']) # en,回退到 defaults
删除也只作用于第一个映射。如果键不在第一个映射中,即使后面的映射有,也会抛出 KeyError。
python
try:
del config['theme'] # theme 不在 env 中,但在 user/defaults 中
except KeyError as e:
print('KeyError:', e)
2.4 动态性
ChainMap 是视图,不是副本。修改底层映射会立即反映到 ChainMap 上。
bash
d1 = {'a': 1}
d2 = {'b': 2}
cm = ChainMap(d1, d2)
print(cm['a']) # 1
d1['a'] = 100
print(cm['a']) # 100
d2['c'] = 3
print(cm['c']) # 3
3. 常用属性与方法
| 属性/方法 | 说明 |
|---|---|
maps |
底层映射列表,按优先级从高到低排列。可修改。 |
new_child(m=None, **kwargs) |
返回新的 ChainMap,在开头插入一个新映射。 |
parents |
属性,返回新的 ChainMap,包含除第一个映射外的所有映射。 |
__getitem__ |
按顺序查找,返回第一个匹配值。 |
__setitem__ |
只写入 maps[0]。 |
__delitem__ |
只从 maps[0] 删除。 |
__contains__ |
任意映射包含该键则返回 True。 |
__iter__ |
迭代所有唯一键。 |
__len__ |
返回唯一键的数量。 |
__bool__ |
任意映射非空则为 True。 |
get(key, default=None) |
查找所有映射,不存在返回默认值。 |
setdefault(key, default=None) |
如果键存在任意映射,返回其值;否则写入 maps[0]。 |
pop(key, default) |
只从 maps[0] 弹出。 |
popitem() |
只从 maps[0] 弹出项。 |
clear() |
只清空 maps[0]。 |
update(other) |
只更新 maps[0]。 |
copy() |
浅拷贝,底层映射共享。 |
4. new_child 与 parents
这两个方法常用于模拟作用域链。
python
base = ChainMap({'a': 1}, {'b': 2})
child = base.new_child({'a': 10, 'c': 3})
print(child['a']) # 10
print(child['b']) # 2
print(child.maps)
# [{'a': 10, 'c': 3}, {'a': 1}, {'b': 2}]
print(child.parents.maps)
# [{'a': 1}, {'b': 2}]
-
new_child():创建一个新作用域,优先级最高。 -
parents:返回父作用域链,去掉当前最高优先级映射。
Python 3.10+ 中 new_child 还支持关键字参数:
python
child = base.new_child(a=10, c=3)
5. 示例:配置分层
python
from collections import ChainMap
defaults = {'theme': 'dark', 'language': 'en', 'show': True}
user = {'theme': 'light'}
env = {'language': 'zh'}
config = ChainMap(env, user, defaults)
print(config['theme']) # light
print(config['language']) # zh
print(config['show']) # True
config['theme'] = 'blue' # 写入 env
print(env) # {'language': 'zh', 'theme': 'blue'}
print(user) # {'theme': 'light'}
print(config['theme']) # blue
del config['language'] # 只从 env 删除
print(config['language']) # en,回退到 defaults
6. setdefault 与 pop 的特殊行为
setdefault
python
cm = ChainMap({}, {'x': 1})
print(cm.setdefault('x', 100)) # 1,来自第二个映射
print(cm.maps[0]) # {},不会写入第一个映射
print(cm.setdefault('y', 200)) # 200
print(cm.maps[0]) # {'y': 200}
如果键存在于任何映射中,setdefault 不会写入第一个映射。
pop
bash
cm = ChainMap({'x': 1}, {'x': 2, 'y': 3})
print(cm.pop('x')) # 1,只从第一个映射弹出
print(cm['x']) # 2,来自第二个映射
try:
cm.pop('y') # y 不在第一个映射中
except KeyError:
print('KeyError')
pop、popitem、clear、update、del 都只作用于第一个映射。
7. 与 dict 合并对比
bash
merged = {**defaults, **user, **env} # 静态副本,修改不会反映
chain = ChainMap(env, user, defaults) # 动态视图,内存高效
-
dict合并会复制所有键值。 -
ChainMap只保存映射引用,查找时按顺序搜索。 -
ChainMap写入只影响第一个映射。 -
ChainMap的底层映射变化会动态反映。
8. 典型应用场景
-
配置管理:默认配置、用户配置、环境变量、命令行参数分层覆盖。
-
模拟作用域:局部作用域、全局作用域、内置作用域。
-
模板上下文 :如 Django 的
Context类似机制。 -
只读叠加视图:需要按优先级查看多个字典,但不想合并复制。
-
临时覆盖:创建子上下文,不污染原始字典。
9. 注意事项
-
写入只影响第一个映射
cm['x'] = 1等价于cm.maps[0]['x'] = 1。 -
删除、pop、popitem、clear、update 只作用于第一个映射
即使后面的映射有该键,也不会被删除或更新。
-
迭代顺序不保证与查找顺序一致
list(ChainMap({'a': 1}, {'b': 2}))可能得到['b', 'a']。重复键只出现一次。 -
len返回唯一键数量pythoncm = ChainMap({'a': 1}, {'a': 2, 'b': 3}) print(len(cm)) # 2 -
in检查所有映射pythoncm = ChainMap({'a': 1}, {'b': 2}) print('b' in cm) # True -
底层映射修改会影响
ChainMap因为它是视图,不是副本。
-
不是线程安全的
多线程环境下需要额外同步。
-
可以子类化实现
__missing__pythonclass MyChainMap(ChainMap): def __missing__(self, key): return 'default' cm = MyChainMap({'a': 1}) print(cm['b']) # default -
copy()是浅拷贝底层映射对象仍然共享。
-
ChainMap不支持|合并运算符Python 3.9+ 的
dict支持|,但ChainMap没有。
10. 总结
ChainMap 是一个轻量级的"多字典视图",适合分层查找、配置覆盖和模拟作用域。它不复制数据,查找按 maps 顺序进行,写入只作用于第一个映射。理解它的"视图"本质和"只写第一层"的规则,就能正确高效地使用它。