# Python 3.15 多了 frozendict，默认配置还要死守 MappingProxyType 吗？

**URL:** <https://www.shiqidu.com/t/topic/1932>\
**Category:** 开发调优\
**Tags:** Python, 编程\
**Created:** [2026 年11 月 4 日 10:46 UTC](https://www.shiqidu.com/t/topic/1932 "2026-11-04T10:46:07Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2026 年11 月 4 日 10:46 UTC](https://www.shiqidu.com/t/topic/1932/1 "2026-11-04T10:46:07Z")

</div>

`isinstance(cfg, dict)` 忽然红了，上一行才刚把模块级默认配置从普通 `dict` 换成 `frozendict`。换的人说 3.15 内置了、还能当字典键。另一些老代码还在用 `types.MappingProxyType` 包一层只读视图。新类型来了，旧写法要不要连夜扔掉，先看官方怎么定义行为。

文档树是 Python **3.15.2** 。[What’s New in Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html) 的 PEP 814 一节，和 [PEP 814](https://peps.python.org/pep-0814/) 正文，是下面说法的来源。

## frozendict 实际保证什么

What’s New 写得很短：`frozendict` 进 `builtins`。创建后不能改。 **不是** `dict` 子类，直接继承 `object`。键和值都可哈希时，整个映射可哈希。保留插入顺序，但相等比较不看顺序。官方示例里，对 frozendict 做 `a['z'] = 3` 会 `TypeError`。调换构造顺序的两个实例可以 `==` 且 `hash` 相同。

PEP 814 补了使用场景：当字典键或 `set` 元素、丢给 `@functools.lru_cache` 的参数、以及函数默认参数——避免可变默认值坑。构造 API 接近 `dict`：空、关键字、从映射或键值对可迭代对象创建。`|` 合并、`|=` 都会得到 **新的** frozendict，不会原地改左边那个。查找复杂度按 PEP 的说法是 O(1) 量级（相对曾经提案里另一种有序度不同的 frozen map）。

标准库一侧，What’s New 列了已接受 frozendict 的模块，包括 `json`、`copy`、`pickle`、`pprint`、`marshal`、`decimal`、`plistlib`（序列化）、`xml.etree.ElementTree` 等。`eval` / `exec` 的 globals、以及 `type()` / `str.maketrans()` 也能吃。新代码里把「默认配置 / 常量映射」建成 frozendict，和这些路径是对齐的。

## isinstance 为什么会翻车

官方点名提醒：靠 `isinstance(arg, dict)` 做分支的代码，要改成 `isinstance(arg, (dict, frozendict))`，或者直接 `isinstance(arg, collections.abc.Mapping)`，后者连 `MappingProxyType` 等其它映射也能认。

这不是小疏忽。大量序列化辅助、配置合并、`dict(cfg)` 浅拷贝假设，都默认「映射就是 dict」。frozendict 故意不继承 `dict`，PEP 里 Rejected Ideas 写过原因：若继承，`dict. __setitem__ (frozendict, …)` 这类通道很难堵干净。所以类型检查要跟着改，而不是抱怨「为什么不像 tuple 对 list 那样」。

值如果是 list 之类可变对象，frozendict 只保证浅层不可改。PEP 的 `copy` 例子里，浅拷贝仍共享那个 list，`deepcopy` 才会断开——和「不可变映射里塞可变值」的老坑一样，别指望一层 frozendict 冻住整棵对象树。线程安全一节也只承诺浅层不可变创建后可跨线程共享。值本身若还能被别的线程改，问题仍在值上。

和 `frozenset` 的类比只在「不可变、可哈希」这一层相近：一个冻的是集合，一个冻的是映射。需要「一组不可变键」时仍用 frozenset。需要「键值对常量、还可能当别的 dict 的键」时才轮到 frozendict。别把所有常量容器都改成同一种类型。

## MappingProxyType 先别踩一脚踢开

PEP 专门提了和 `types.MappingProxyType` 的关系：3.3 就有的只读视图，不可哈希，也不能当继承基类。通过 `gc.get_referents()` 之类还能摸回原来的可变 dict。它适合「已有一个 dict，临时借只读外壳」的存量模式，比如函数返回「别改我内部表」的视图。

新代码若目标版本已经是 3.15+，默认配置、要进 cache 键的复合映射，优先 frozendict 更合适。库要兼容 3.14 及更早，继续对外返回 MappingProxyType、或文档写明「3.15+ 可用 frozendict」，都比强行在旧解释器假造 builtins 干净。存量模块里已经用 Proxy 包住的常量，没有哈希需求就不必为了「追新」整页替换——行为语义和 frozendict 并不完全同一套，硬换只会制造无收益的 diff。

升级检查可以很短：搜 `isinstance(..., dict)` 和「假定 `.update` / ` __setitem__ ` 一定存在」的分支。模块级可变全局 dict 改成 frozendict 或构造后立刻冻住。需要当键的嵌套结构确认值也可哈希。第三方包若仍只认 `dict`，就在边界 `dict(frozen)` 出一份可变副本，别在库内部偷偷解冻。

你们仓库里，配置常量是已经冻住了，还是还在依赖「大家约定别改」？
