能否基于对象标识使用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
相关产品推荐
相关产品推荐

