bigywater.tech
新版本特性 · 3

Python 3.15 内置 frozendict 实测:不是 dict 的子类,能当 lru_cache 的参数,哈希值会缓存,内存和 dict 几乎一样

“Python 3.15 的 frozendict 和 dict、MappingProxyType 到底差在哪、值不值得换” 是新内置类型最该先搞清楚的问题。这篇在本机用 3.14.9 和 3.15.0 并排跑,逐项试它的不可变性、可哈希性、和视图的区别,并量了内存和速度。

不是 dict 子类
isinstance(frozendict(), dict) 是 False,但是 collections.abc.Mapping
26040 字节
1000 个键的 frozendict,对比 dict 的 26032 字节
5269.0 μs
10 万个键时 tuple(sorted(d.items())) 的耗时;frozendict 同一对象再次 hash 约 0.03 μs

存在与类型

实测 · 本机执行输出
  Python 3.14(退出码 1):
      Traceback (most recent call last):
        File "<tmp>/s1.py", line 3, in <module>
          fd = frozendict(a=1, b=2)
               ^^^^^^^^^^
      NameError: name 'frozendict' is not defined. Did you mean: 'frozenset'?
  Python 3.15(退出码 0):
      repr: frozendict({'a': 1, 'b': 2})
      type: frozendict | __mro__: ['frozendict', 'object']
      isinstance(fd, dict) = False | isinstance(fd, abc.Mapping) = True | isinstance(fd, abc.MutableMapping) = False
      空的: frozendict() | 从键值对: frozendict({'x': 1}) | 合并关键字: frozendict({'x': 1, 'y': 2})
      fd == {"a": 1, "b": 2} -> True | 顺序不同 -> True
      frozendict[str, int] -> frozendict[str, int]

3.14 里 NameError;3.15 里是内置类型,不是 dict 的子类,是 Mapping,不是 MutableMapping。

不可变

实测 · 本机执行输出
  Python 3.15(退出码 0):
      fd['c'] = 3                  -> TypeError: 'frozendict' object does not support item assignment
      del fd['a']                  -> TypeError: 'frozendict' object does not support item deletion
      fd.update(c=3)               -> AttributeError: 'frozendict' object has no attribute 'update'
      fd.pop('a')                  -> AttributeError: 'frozendict' object has no attribute 'pop'
      fd.clear()                   -> AttributeError: 'frozendict' object has no attribute 'clear'
      fd.setdefault('z', 1)        -> AttributeError: 'frozendict' object has no attribute 'setdefault'
      fd.popitem()                 -> AttributeError: 'frozendict' object has no attribute 'popitem'
      dict 有而 frozendict 没有的方法: ['clear', 'pop', 'popitem', 'setdefault', 'update']
      frozendict 的公开方法: ['copy', 'fromkeys', 'get', 'items', 'keys', 'values']
      fd | {"b": 9, "c": 3} -> frozendict({'a': 1, 'b': 9, 'c': 3})
      {"b": 9} | fd         -> {'b': 2, 'a': 1} (左边是 dict,结果还是 dict)
      fd |= {"x": 1} 之后:原来的 fd 仍是 frozendict({'a': 1, 'b': 2}) ,g 是新对象 frozendict({'a': 1, 'b': 2, 'x': 1})

赋值和删除是 TypeError;update、pop、clear、setdefault、popitem 不存在;| 返回新的 frozendict。

可哈希

实测 · 本机执行输出
  Python 3.15(退出码 0):
      hash 相等(键顺序不同): True
      当 dict 的键: {frozendict({'a': 1, 'b': 2}): 'v'} | 放进 set: {frozendict({'a': 1, 'b': 2})}
      值里有 list: TypeError: unhashable type: 'list'
      lru_cache 传 frozendict: 2 CacheInfo(hits=0, misses=1, maxsize=128, currsize=1)
      lru_cache 传 dict: TypeError: unhashable type: 'dict'
      dataclass 字段默认值写 {}: ValueError: mutable default <class 'dict'> for field x is not allowed: use default_factory
      dataclass 字段默认值写 frozendict(): D(x=frozendict()) | 两个实例共享同一个默认对象: True

能当键、放进 set、传给 lru_cache、当 dataclass 默认值;值必须都可哈希。

和只读视图的区别

实测 · 本机执行输出
  Python 3.15(退出码 0):
      底层 dict 加了 b 之后:MappingProxyType 看到 {'a': 1, 'b': 2} | frozendict 看到 {'a': 1}
      hash(MappingProxyType): TypeError: unhashable type: 'dict'
      hash(frozendict) 能算: True
      frozendict == MappingProxyType: True
      copy.copy(fd) is fd: True | copy.deepcopy(fd) is fd: False | deepcopy 后相等: True
      json.dumps(fd): {"a": 1}
      pickle 往返: frozendict({'a': 1, 'b': [1, 2]})
      match 映射模式能用,rest = {}
      fd.keys() & {"a"} = {'a'} | 顺序: ['z', 'a']

MappingProxyType 随底层变化且不可哈希;frozendict 是独立副本。

代价

实测 · 本机执行输出
                                       10 个键     1000 个键    100000 个键
  占内存 dict (字节)                          272       26032      3844864
  占内存 frozendict (字节)                    280       26040      3844872
  构造 dict(d) (μs)                        0.1         2.2        369.5
  构造 frozendict(d) (μs)                  0.1         2.2        360.0
  读取 d[k] (μs)                         0.028       0.027        0.027
  读取 fd[k] (μs)                        0.029       0.027        0.028
  读取 mp[k] (μs)                        0.032       0.030        0.031
  遍历 d (μs)                              0.2        11.2        860.7
  遍历 fd (μs)                             0.2        11.4        846.5
  构造 + 第一次 hash(fd) (μs)                 0.2         7.2        838.4
  同一个 fd 再次 hash (μs)                  0.033       0.033        0.032
  tuple(sorted(d.items())) (μs)          0.4        43.7       5269.0

内存和读取与 dict 基本一样;哈希值缓存,第二次起几乎免费。

分步演示

frozendict 实测
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Homebrew 的 Python 3.14.9 与 3.15.0(同一台机器上两个解释器并排跑同一份代码;两者都不是自由线程构建,也都没有启用 JIT);每个例子是临时目录里的独立小脚本,用子进程运行并取真实的 stdout / stderr(路径换成 );计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇语法、报错文本用子进程取真实输出;代价一节用 timeit,每项取 7 次重复里的最小值,规模 10 / 1000 / 100000 个键,字符串键
    • 没有验证:值是更复杂对象时的内存与速度;哈希缓存的实现位置(只量到第二次几乎免费,没有看源码);多线程下的行为;和第三方 frozendict 包、immutables.Map 的比较;类型检查器、typing 里对 frozendict 的支持;内置 dict 的子类化与 frozendict 的互操作细节;PEP 的完整规范(本篇只验证了行为)
    京ICP备2026067502号-1京公网安备11010502063487号