登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python 3.15 frozendict 怎么用于缓存键:不可变映射、哈希条件与浅拷贝边界

来源:17golang原创

时间:2026-09-03 13:27:09 422浏览 收藏

缓存函数若接收一组可选参数,普通 dict 不能直接作为 lru_cache 的参数;手工转成排序元组又容易丢失映射语义。Python 3.15 新增内置 frozendict,它保留映射接口,同时禁止创建后修改外层条目。真正决定它能否成为缓存键的,不只是“不可变”三个字,而是全部键和值都必须可哈希。

要点速览
  • frozendict 是内置不可变映射,不是 dict 子类,但实现标准 Mapping 协议。
  • 所有键和值都可哈希时,整个映射才有哈希值,才能安全交给 lru_cache
  • 不可变只约束外层映射;构造和 copy() 都是浅拷贝,嵌套列表仍可变化。

最小写法:直接把配置映射交给缓存

frozendict 位于 builtins,无需额外导入。它可以接收关键字参数、普通字典或键值对迭代器,并保留插入顺序。下面的调用参数只有字符串和整数,因而整个映射可哈希:

from functools import lru_cache

@lru_cache(maxsize=256)
def build_query(options: frozendict[str, object]) -> str:
    return f"region={options['region']}&retries={options['retries']}"

options = frozendict(region="cn", retries=2)
print(build_query(options))

这种写法保留了 options['region'] 的可读性,也避免调用方在函数执行期间增删键。缓存规模变大后,参数边界越统一,越容易检查同一逻辑请求是否确实命中同一个缓存条目。

缓存键为什么要求所有键和值都可哈希

映射的键本来就必须可哈希;frozendict 进一步要求值也可哈希,整个对象才会得到哈希值。这里可以把调用参数先收敛成 frozendict,再由可哈希键值共同决定哈希值,供 lru_cache 定位缓存条目。

good = frozendict(region="cn", retries=2)
bad = frozendict(region="cn", headers=["Accept"])

hash(good)  # 可用
hash(bad)   # TypeError:列表不可哈希

因此,“外层不能改”不等于“自动可哈希”。列表、普通字典、集合等可变值必须先转换成合适的不可变表示,例如把顺序有意义的列表改成元组。不要为了绕过异常把复杂对象直接转字符串,否则等价性和碰撞边界会变得难以审查。

frozendict 可哈希条件与 lru_cache 缓存条目的静态关系框图
图1:查看参数边界、哈希条件和缓存结构三组关系;重点确认调用参数先成为 frozendict,且可哈希键值共同产生哈希值后,lru_cache 才能定位缓存条目。

顺序无关的相等性让等价配置命中一致

frozendict 保留插入顺序,遍历时仍按写入顺序返回;但相等比较和哈希计算不依赖条目顺序。相同键值用不同顺序构造,仍代表同一个映射内容:

a = frozendict(region="cn", retries=2)
b = frozendict(retries=2, region="cn")

assert a == b
assert hash(a) == hash(b)

这正适合“配置内容相同就共享结果”的缓存语义。若业务把顺序本身视为数据,例如一组有优先级的规则,就应把顺序放进元组值里,而不是依赖映射条目的插入顺序区分缓存键。

检查项适合做缓存键需要改造
值的类型字符串、数字、元组等可哈希对象列表、普通字典、集合
配置等价性只关心键值内容条目写入顺序具有业务含义
更新方式创建新的映射值依赖原地修改

外层不可变不代表嵌套值被冻结

frozendict(source) 会对源字典条目做 O(n) 的浅拷贝。外层键集合固定下来,但源字典和新映射仍可能引用同一个嵌套列表:

source = {"headers": ["Accept"]}
frozen = frozendict(source)

source["headers"].append("X-Trace")
assert frozen["headers"] == ["Accept", "X-Trace"]

frozen.copy() 同样是浅拷贝;在 CPython 中,它只是返回同一对象的新引用。若业务快照要求嵌套对象彼此隔离,可使用 copy.deepcopy() 获得独立深拷贝,或在构造前把列表递归转换为元组。后者还能让映射满足缓存键的哈希条件。

源字典、浅拷贝、frozendict 外层键集合和嵌套列表关系框图
图2:查看源对象、浅层边界和隔离副本;浅拷贝固定 frozendict 的外层键集合,却仍共享嵌套列表,只有 deepcopy 才建立独立副本。

更新配置时创建新值,而不是寻找修改入口

frozendict 没有 update()pop()clear() 和条目赋值能力。需要派生配置时可使用合并运算符,右侧同名键覆盖左侧,并返回新的 frozendict

base = frozendict(region="cn", retries=2)
tuned = base | {"retries": 3}

assert base["retries"] == 2
assert tuned["retries"] == 3

|= 看起来像原地更新,实际也是创建新对象并重新绑定变量,旧引用不会改变。这个代价换来的是清晰的配置版本边界;但若高频循环里持续构造大型映射,应先评估 O(n) 复制成本,不要把不可变对象当成零成本替代品。

常见问题

frozendict 里能放列表吗?

能。值可以不可哈希,因此列表可以保存;但此时整个 frozendict 不能哈希,也就不能直接作为 lru_cache 参数或其他字典的键。

它能直接替换所有 dict 参数吗?

不能。它不是 dict 子类。只读接口最好接收 collections.abc.Mapping;依赖 isinstance(value, dict) 或修改方法的代码需要单独兼容。

Python 3.15.0rc2 仍是候选预览版,官方不建议直接用于生产环境。现在更适合在隔离环境验证:缓存参数是否全部可哈希、嵌套值是否已冻结,以及第三方库是否接受 Mapping。三个条件都明确后,frozendict 才能真正成为稳定的缓存键。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>