Python的dict天生可变,赋值给常量名(如CONFIG)仅靠约定无法阻止修改;types.MappingProxyType提供只读视图但不阻断原字典修改,真正不可变需封装私有字典并仅暴露代理。

为什么直接用 dict 无法保证不可变性
Python 的 dict 天生可变:哪怕你把它赋值给一个常量名(比如 CONFIG),只要有人拿到这个对象引用,就能调用 .update()、del 或直接赋值修改。所谓“只读”只是靠约定,不是语言层保障。
常见错误现象包括:
- 函数内部意外修改了传入的配置字典
- 多个模块共享一个字典,某处改了影响全局行为
- 单元测试因状态污染而偶发失败
types.MappingProxyType 是什么,它能做什么
types.MappingProxyType 是一个轻量包装器,它把一个普通 dict 包装成只读视图——不支持增删改,但支持所有只读操作(__getitem__、.get()、.keys()、.items() 等)。
关键点:
立即学习“Python免费学习笔记(深入)”;
- 它不复制底层字典,只是代理访问,内存开销极小
- 如果原字典被外部修改,
MappingProxyType视图会立刻反映变更(即它是实时视图,不是快照) - 它本身不可被反向解包或强制转回可变字典(没有公开 API 能绕过限制)
示例:
from types import MappingProxyType
<p>original = {"host": "localhost", "port": 8080}
proxy = MappingProxyType(original)</p><p>print(proxy["host"]) # ✅ 正常访问
proxy["timeout"] = 30 # ❌ TypeError: 'mappingproxy' object does not support item assignment
original["timeout"] = 30 # ✅ 原字典仍可改(这是设计使然)
print(proxy["timeout"]) # ✅ 现在能读到新键(视图同步更新)
如何真正实现“不可变字典”的常用模式
如果目标是彻底禁止修改(包括防止原字典被篡改),仅靠 MappingProxyType 不够——必须切断对原始 dict 的引用。典型做法是封装 + 私有化:
⚠️ 已弃用 — 该技能已拆分为两个独立技能,以提升模块化程度:**webchat-https-proxy**(HTTPS/WSS 反向代理)和 **webchat-voi...**
- 把原始字典定义在函数/类作用域内,不暴露出去
- 只返回
MappingProxyType实例 - 避免使用全局可变字典作为源
推荐写法:
from types import MappingProxyType
<p>def get_config():
_config_dict = {"db_url": "sqlite://", "debug": False}
return MappingProxyType(_config_dict)</p><p>CONFIG = get_config() # ✅ 外部拿不到 _config_dict 引用
注意:_config_dict 在函数返回后即失去外部引用,GC 可回收;CONFIG 是安全的只读视图。
对比陷阱写法:
CONFIG_DICT = {"a": 1}
CONFIG = MappingProxyType(CONFIG_DICT) # ❌ CONFIG_DICT 仍可被任意模块修改
和 frozenmap、immutables.Map 等第三方方案比有什么区别
MappingProxyType 最大特点是零依赖、标准库内置、无额外性能损耗。但它不是“冻结”而是“代理”,所以:
- 不提供哈希支持(不能用作
dict的 key) - 不阻止源字典被改——这点常被忽略,也是最易踩的坑
- 没有深冻结能力(嵌套字典仍是可变的)
如果你需要:
- 真正不可变(包括嵌套结构)→ 考虑
immutables.Map或pyrsistent - 要哈希 → 必须用
tuple手动构造或选支持哈希的第三方类型 - 只想快速封住配置字典且控制源引用 →
MappingProxyType加封装是最简可靠解
复杂点在于:它看起来像“冻结”,实际只是“只读门禁”。是否真安全,取决于你有没有守住那扇背后的门——也就是原始 dict 的生命周期和可见性。

















