缓存极大地加速了从 CPU 级操作到数据库接口的处理速度。 缓存失效(确定何时删除缓存数据)是一项复杂的挑战。这篇文章解决了一个更简单但隐蔽的缓存问题。
这个问题潜伏了 18 个月,只有当用户偏离推荐的使用模式时才浮出水面。 该问题源于我组织内的自定义机器学习 (ML) 框架(基于 scikit-learn 构建)。 该框架频繁访问多个数据源,需要缓存层来优化性能和成本(降低 BigQuery 出口成本)。
最初使用lru_cache
,但开发过程中经常访问的静态数据需要持久化缓存。 DiskCache
是一个使用 SQLite 的 Python 库,因其简单性以及与我们的 32 进程环境和 Pandas DataFrame(高达 500MB)的兼容性而被选中。 在顶部添加了一个 lru_cache
层以进行内存访问。
随着越来越多的用户尝试该框架,问题出现了。 随机报告了不正确的结果,难以一致地重现。 根本原因:就地修改缓存的 Pandas DataFrame。
我们的编码标准规定在任何处理后创建新的数据帧。 然而,有些用户出于习惯,使用inplace=True
,直接修改缓存对象。 这不仅改变了它们的即时结果,还破坏了缓存的数据,影响后续请求。
为了说明这一点,请考虑这个使用字典的简化示例:
<code class="language-python">from functools import lru_cache import time import typing as t from copy import deepcopy @lru_cache def expensive_func(keys: str, vals: t.Any) -> dict: time.sleep(3) return dict(zip(keys, vals)) def main(): e1 = expensive_func(('a', 'b', 'c'), (1, 2, 3)) print(e1) e2 = expensive_func(('a', 'b', 'c'), (1, 2, 3)) print(e2) e2['d'] = "amazing" print(e2) e3 = expensive_func(('a', 'b', 'c'), (1, 2, 3)) print(e3) if __name__ == "__main__": main()</code>
lru_cache
提供参考,而不是副本。 修改e2
会改变缓存的数据。
解决方案:
解决方案涉及返回缓存对象的深层副本:
<code class="language-python">from functools import lru_cache, wraps from copy import deepcopy def custom_cache(func): cached_func = lru_cache(func) @wraps(func) def _wrapper(*args, **kwargs): return deepcopy(cached_func(*args, **kwargs)) return _wrapper</code>
这会增加少量开销(数据重复),但可以防止数据损坏。
要点:
lru_cache
的引用行为。以上是Python 缓存可变值的详细内容。更多信息请关注PHP中文网其他相关文章!