Python functools.lru_cache 实用指南:用法、原理与 5 个常见陷阱

举报
yd_247334186 发表于 2026/11/03 09:10:58 2026/11/03
【摘要】 functools.lru_cache 是 Python 标准库里性价比最高的性能优化工具之一:加一行装饰器,就能把重复计算变成字典查找。但它用错了也会带来内存泄漏、结果错乱等隐蔽问题。本文用可运行的例子讲清楚它的用法和常见坑。 一、基本用法from functools import lru_cacheimport time@lru_cache(maxsize=None)def fib(n:...

functools.lru_cache 是 Python 标准库里性价比最高的性能优化工具之一:加一行装饰器,就能把重复计算变成字典查找。但它用错了也会带来内存泄漏、结果错乱等隐蔽问题。本文用可运行的例子讲清楚它的用法和常见坑。

一、基本用法

from functools import lru_cache
import time

@lru_cache(maxsize=None)
def fib(n: int) -> int:
    return n if n < 2 else fib(n - 1) + fib(n - 2)

start = time.perf_counter()
print(fib(100))                  # 354224848179261915075
print(f"{time.perf_counter() - start:.4f}s")
print(fib.cache_info())          # CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)

不加缓存时,fib(100) 的递归调用次数是天文数字;加上之后每个 n 只算一次。

  • maxsize=None:不限制容量,等价于 Python 3.9+ 的 functools.cache。
  • maxsize=128(默认):超过容量时淘汰"最近最少使用"(LRU)的条目。
  • cache_info() 查看命中率,cache_clear() 清空缓存。

二、原理一句话

装饰器把函数的位置参数和关键字参数拼成一个键,存进内部字典;maxsize 有限时再用双向链表维护使用顺序。所以参数必须可哈希,并且函数应当是纯函数——相同输入永远得到相同输出。

三、5 个常见陷阱

陷阱 1:参数不可哈希

@lru_cache
def total(items):
    return sum(items)

try:
    total([1, 2, 3])
except TypeError as e:
    print(e)          # unhashable type: 'list'

print(total((1, 2, 3)))  # 6,改用元组即可

调用方传 list、dict 时会直接报错。解决办法是在外层把参数转换成 tuple 或 frozenset 再调用缓存函数。

陷阱 2:返回可变对象被调用方修改

@lru_cache
def default_config():
    return {"timeout": 30}

cfg = default_config()
cfg["timeout"] = 1          # 改的是缓存里的同一个对象!
print(default_config())      # {'timeout': 1}

缓存返回的是同一个对象。返回 dict/list 时,要么返回不可变类型(如 types.MappingProxyType、tuple),要么让调用方自己 copy()。

陷阱 3:装饰实例方法导致内存泄漏

class Report:
    @lru_cache(maxsize=None)
    def render(self, page):
        return f"page {page}"

self 会成为缓存键的一部分,缓存是类级别共享的,于是每个 Report 实例都被缓存强引用,永远不会被回收。推荐做法:

from functools import cached_property

class Report:
    def __init__(self, pages):
        self.pages = pages

    @cached_property
    def summary(self):          # 结果存在实例自己的 __dict__ 里
        print("computing...")
        return f"{len(self.pages)} pages"

r = Report([1, 2, 3])
print(r.summary)   # computing... 3 pages
print(r.summary)   # 3 pages(不再计算)

无参数的属性用 cached_property;需要参数时,可以在 __init__ 里为每个实例单独包装:self.render = lru_cache(maxsize=256)(self._render),缓存随实例一起释放。

陷阱 4:缓存依赖外部状态的函数

RATE = 7.0

@lru_cache
def to_cny(usd):
    return usd * RATE

print(to_cny(10))   # 70.0
RATE = 7.2
print(to_cny(10))   # 仍然是 70.0

函数读了全局变量、文件、数据库或当前时间,缓存就会返回过期结果。要么把这些依赖变成参数,要么在依赖变化时调用 to_cny.cache_clear()。

陷阱 5:1 和 1.0 可能被当成同一个键

@lru_cache
def add(a, b):
    return a + b

print(add(1, 2))     # 3
print(add(1.0, 2))   # 3 ——命中了上一次的缓存,而不是 3.0

因为 (1, 2) == (1.0, 2) 且哈希相同。(单个 int/str 参数时 CPython 走了快速路径,未必复现,但不要依赖这个实现细节。)如果类型会影响结果,使用 @lru_cache(typed=True),它会把参数类型也纳入键中,此时 add(1.0, 2) 返回 3.0。

四、什么时候用、怎么选 maxsize

适合:纯计算(递归、动态规划、解析配置)、重复查询的慢接口(配合定期 cache_clear())。
不适合:有副作用的函数、依赖实时数据的函数、参数几乎不重复的函数(只会白白占内存)。

maxsize 建议:参数空间有限、可控时用 None;参数空间很大时设一个上限(如 1024),然后观察 cache_info() 的命中率再调整。

五、小结

  1. 一行 @lru_cache 就能消除重复计算,用 cache_info() 验证效果;
  2. 参数要可哈希,函数要是纯函数;
  3. 返回可变对象要小心被修改;
  4. 实例方法优先用 cached_property 或按实例包装,避免内存泄漏;
  5. 类型敏感时加 typed=True。

以上代码均可在 Python 3.9+ 直接运行,欢迎复制试一试。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。