You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否基于对象标识使用Python functools的@cache装饰器?

基于对象身份的Python缓存装饰器实现方案

完全可以实现,核心思路是绕开参数对象自定义的__hash__/__eq__判断逻辑,直接用对象生命周期内唯一的身份标识做缓存键匹配。


核心原理

Python内置的id()函数会返回每个对象在存活期间的唯一整数标识(CPython解释器下对应对象的内存地址):两个独立创建的实例哪怕值完全相等、__eq__判断为真,id()返回值也一定不同。我们只要把缓存的匹配键从参数对象本身替换为参数的身份标识,就能实现按对象身份命中缓存的需求。

默认的functools.cache无法满足需求的原因是:它底层直接用参数对象的__hash__计算哈希值、__eq__判断键是否重复,只要两个对象逻辑相等就会命中缓存。


实现代码

最简实现(适合自定义类参数场景)

如果你的传入参数都是自定义类实例,用这个轻量版本即可,全程不会触发参数的__hash__和__eq__方法:

from functools import wraps

def identity_cache(func):
    cache = {}
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 构造基于id的缓存键,完全不调用参数的哈希、相等方法
        cache_key = (
            tuple(id(arg) for arg in args),
            tuple((k, id(v)) for k, v in sorted(kwargs.items()))
        )
        if cache_key not in cache:
            cache[cache_key] = func(*args, **kwargs)
        return cache[cache_key]
    return wrapper

把示例代码里的@cache替换为@identity_cache后运行,输出结果如下,完全符合预期:

Computing foo(a) = 1
foo(ka): 1
Computing foo(a) = 2
foo(ka2): 2

运行全程不会打印Key类里定义的__hash__/__eq__日志,因为逻辑里根本没有调用这两个方法,ka和ka2哪怕值相等,因为身份不同也不会复用缓存。


优化版(解决内存泄漏问题)

上面的最简实现用普通字典强引用缓存键,会导致所有传入过的参数对象无法被垃圾回收,长生命周期程序下可能出现内存泄漏。如果需要在生产环境使用,可以搭配弱引用改造,对象被回收时自动删除对应的缓存条目:

from functools import wraps
import weakref

def identity_cache(func):
    cache = {}

    def make_key(args, kwargs):
        pos_keys = []
        for arg in args:
            try:
                pos_keys.append(weakref.ref(arg))
            except TypeError:
                # 不可弱引用的类型(如列表、字典、小整数等)直接用id做键
                pos_keys.append(("id_tag", id(arg)))
        
        kw_keys = []
        for k, v in sorted(kwargs.items()):
            try:
                kw_keys.append((k, weakref.ref(v)))
            except TypeError:
                kw_keys.append((k, ("id_tag", id(v))))
        
        final_key = (tuple(pos_keys), tuple(kw_keys))
        # 给所有弱引用绑定回收回调,对象销毁时自动清缓存
        def _cleanup(ref):
            cache.pop(final_key, None)
        for item in pos_keys:
            if isinstance(item, weakref.ReferenceType):
                item.__callback__ = _cleanup
        for _, item in kw_keys:
            if isinstance(item, weakref.ReferenceType):
                item.__callback__ = _cleanup
        return final_key

    @wraps(func)
    def wrapper(*args, **kwargs):
        cache_key = make_key(args, kwargs)
        if cache_key not in cache:
            cache[cache_key] = func(*args, **kwargs)
        return cache[cache_key]
    
    return wrapper

注意事项

  • 受Python解释器的驻留优化机制影响,小整数、短字符串等字面量值如果相等,可能指向同一个内存对象(id相同),这种情况下会命中缓存,属于符合身份判断逻辑的正常现象。
  • 如果需要支持缓存清空、缓存大小统计等functools.cache自带的能力,可以在装饰器里给wrapper函数追加对应的方法即可。

内容的提问来源于stack exchange,提问作者user118967

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 00:36:20