如何让Python functools.lru_cache的返回值变为不可变?
解决方案
functools.lru_cache缓存的是函数返回对象的引用,直接修改引用指向的可变对象(如list、dict)时,缓存内存储的对象也会被同步修改,导致后续调用拿到错误结果。你可以通过以下方案把缓存返回值转为不可变,触发非法修改时直接抛错,快速定位问题代码:
方案1:基础不可变转换装饰器(适用于平层返回值)
通过装饰器把常用可变类型转为对应不可变类型,修改时直接触发TypeError:
import functools from types import MappingProxyType def freeze_return_value(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) # 对常用可变类型做不可变转换,可根据业务需求扩展规则 if isinstance(result, list): return tuple(result) elif isinstance(result, dict): return MappingProxyType(result) elif isinstance(result, set): return frozenset(result) return result return wrapper
使用时把该装饰器放在lru_cache下方(靠近函数定义,保证先冻结结果再存入缓存):
@functools.lru_cache(maxsize=None) @freeze_return_value def your_cached_func(*args): # 原有业务逻辑 return [1, 2, 3, {"key": "value"}]
方案2:递归深冻结(适用于嵌套可变结构)
如果返回值是嵌套的可变结构,用递归逻辑做深度冻结:
from typing import Any def deep_freeze(obj: Any) -> Any: if isinstance(obj, list): return tuple(deep_freeze(item) for item in obj) elif isinstance(obj, dict): return MappingProxyType({k: deep_freeze(v) for k, v in obj.items()}) elif isinstance(obj, set): return frozenset(deep_freeze(item) for item in obj) # 自定义可变类型可在这里补充冻结逻辑 return obj # 替换基础装饰器里的转换逻辑即可 def freeze_return_value(func): @functools.wraps(func) def wrapper(*args, **kwargs): return deep_freeze(func(*args, **kwargs)) return wrapper
方案3:返回深拷贝(仅避免缓存污染,不触发报错)
如果不需要定位修改位置,仅保证缓存结果不被篡改,可以给返回值做深拷贝,修改拷贝对象不会影响缓存内的原始值:
import functools import copy def copy_return_value(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return copy.deepcopy(result) return wrapper
注意该方案的装饰器要放在
lru_cache上方(远离函数定义,保证每次从缓存拿到结果后先拷贝再返回给调用方):@copy_return_value @functools.lru_cache(maxsize=None) def your_cached_func(*args): return [1,2,3]
注意事项
- 装饰器顺序不能错,冻结逻辑要先执行再存缓存,拷贝逻辑要先取缓存再执行拷贝
- 自定义类返回值如果要冻结,可以用
dataclasses.dataclass(frozen=True)定义返回对象,或者重写类的__setattr__方法修改时抛错 - Python 3.10+ 中可以直接用
functools.cache替代functools.lru_cache(maxsize=None),以上方案完全通用
内容的提问来源于stack exchange,提问作者linrongbin
相关产品推荐
相关产品推荐

