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

Python functools避免把可变参数放进缓存键的实现方法

来源:17golang原创

时间:2026-09-20 02:17:09 344浏览 收藏

我在给一个按筛选条件生成报告的函数加 functools.lru_cache 时,最先遇到的不是命中率问题,而是调用方习惯传入 listdict。这类对象内容可以改变,不能直接作为字典键。稳妥做法是:在缓存入口外保留自然的可变参数,在内部先递归规范化为稳定的 tuple,再交给缓存函数。

官方地址:https://docs.python.org/3/library/functools.html

要点速览
  • @lru_cache 的参数必须可哈希,list、dict、set 不能直接进入缓存键。
  • 规范化必须保留业务语义:字典按键排序,集合不能误当成有序列表。
  • 数据源或配置变化时用 cache_clear() 失效;缓存返回值也不要暴露可被调用方修改的共享对象。

先把“可变参数”与“缓存身份”分开

lru_cache 用函数参数组成缓存键,而底层缓存需要可哈希对象。直接写成 @lru_cache 后接收列表,会在真正查缓存前抛出 TypeError: unhashable type: 'list';把字典直接传入也有同样问题。更隐蔽的情况是:把列表临时转成元组,却没有明确列表顺序是否影响结果,可能把两个本应相同的查询拆成不同条目。

先问清楚一个边界:哪些字段决定结果?例如报表筛选器中的 regionstatus 和排序字段通常决定结果,页面展示用的备注不应该混进缓存键。

Python functools lru_cache 将 list 和 dict 规范化为 tuple 缓存键的结构说明图
图1:Python lru_cache 缓存键结构说明图,展示可变输入到不可变键的边界。

用递归规范化承接 list、dict 和 set

一个可复用的入口可以把嵌套容器转换成不可变结构。字典要按键排序后再转成键值对元组;集合本身没有稳定顺序,只有在业务上确实“只关心成员、不关心顺序”时才适合排序。

from functools import lru_cache

def freeze(value):
    # 递归消除可变容器,确保缓存键只由稳定对象组成。
    if isinstance(value, dict):
        # 排序让同一组键值不因输入顺序不同而产生两个缓存条目。
        return tuple(sorted((key, freeze(item)) for key, item in value.items()))
    if isinstance(value, (list, tuple)):
        # 列表顺序属于结果语义时,按原顺序保留。
        return tuple(freeze(item) for item in value)
    if isinstance(value, set):
        # 集合无序;元素必须可比较,否则应改用业务定义的稳定排序键。
        return tuple(sorted(freeze(item) for item in value))
    return value

@lru_cache(maxsize=256)
def _build_report(filters_key):
    # 内部函数只接收规范化键,避免 lru_cache 处理 list 或 dict。
    return {"filters": filters_key, "rows": ("cached",)}

def build_report(filters):
    # 对外接口仍接收调用方熟悉的字典。
    return _build_report(freeze(filters))

这里的关键不是“所有容器都转成 tuple”,而是先定义语义。订单号列表若顺序代表优先级,就不能排序;标签集合若只代表成员关系,排序才合理。字典中的键也要本身可比较、可哈希,否则需要为业务对象提供明确的稳定标识。

命中之后还要设计失效和返回值边界

缓存命中只说明“同一个键已有结果”,不说明外部数据永远没变。官方接口提供 cache_info() 查看 hits、misses、maxsize 和 currsize,也提供 cache_clear() 主动清空。配置刷新、字典表切换或租户数据版本变更时,应把清理动作放在同一个更新流程中,而不是等用户偶然遇到旧结果。

另一个容易漏掉的边界是返回值。lru_cache 会保留返回值引用;如果返回一个可变字典,调用方改动它,后续命中可能读到被改过的对象。更安全的选择是返回只读结构、元组,或在对外层复制一份。

对象或动作适合做法需要注意
list按业务顺序递归转 tuple不要为去重而擅自排序
dict排序后的键值对 tuple键和值都要可稳定规范化
set仅在无序语义明确时排序自定义对象需提供稳定排序规则
数据变化调用 cache_clear()与刷新动作绑定,避免旧结果残留
Python lru_cache 通过 cache_info 和 cache_clear 管理缓存失效与不可变返回值的关系图
图2:Python 缓存失效边界说明图,展示观察、清理与返回值安全的关系。

我会用这张清单做最后判断

  • 缓存函数是否纯粹:不要缓存写数据库、发请求等副作用函数。
  • 规范化是否符合业务:顺序、大小写、缺省值和集合语义不能凭感觉改变。
  • 容量是否可控:长驻进程优先设置有限的 maxsize,再用 cache_info() 观察。
  • 失效是否可达:数据源更新、配置切换或租户变更时,是否能触发清理。

如果一个参数很难定义稳定身份,或者结果依赖当前时间、随机数和外部状态,我通常会放弃 lru_cache,改用带版本号的显式缓存层。缓存键设计的目标不是把所有输入都“冻结”,而是让相同业务结果拥有相同身份,让不同结果不会误用旧数据。

常见问题

把 list 转成 tuple 后就一定安全吗?

不一定。还要确认列表顺序是否有业务意义,以及嵌套元素是否全部可哈希;如果结果依赖外部数据,还要补上失效策略。

为什么两个相同字典可能产生不同缓存条目?

如果直接把键值对按输入遍历顺序拼接,顺序不同就可能形成不同键。规范化时按键排序,可以把同一组字典内容收敛到同一个键。

lru_cache 线程安全后还需要自己加锁吗?

它能保护内部缓存结构的一致性,但并不保证并发 miss 时底层函数只执行一次;若函数有副作用,应该改掉缓存设计或在业务层增加去重控制。

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