functools.lru_cache仅实现LRU策略,不记录访问频次、无频次统计接口,且装饰器封装过深无法修改;其底层用双向链表+字典维护时序,不支持LFU所需频次维度,强行改造会破坏缓存一致性。

为什么 functools.lru_cache 不能直接做 LFU 对比
lru_cache 是纯 LRU(最近最少使用)策略,底层用双向链表 + 字典维护访问时序,每次 get 都把键移到链表头。它不记录访问频次,也没有暴露频次统计接口。想拿它“改”成 LFU,行不通——装饰器封装太深,缓存容器不可见,cache_info() 只返回命中/未命中/最大大小,没有频次字段。
常见误操作是试图在被装饰函数里手动计数,比如加全局字典统计参数调用次数,但这会破坏缓存一致性:缓存命中的调用不会进入函数体,计数就漏了。
用 collections.OrderedDict 手写 LRU 缓存(便于对比)
自己实现一个最小可用 LRU,能看清 key 的顺序变化,也方便后续改成 LFU。核心是利用 OrderedDict 的 move_to_end() 和 popitem(last=False):
from collections import OrderedDict
<p>class LRUCache:
def <strong>init</strong>(self, maxsize=128):
self.cache = OrderedDict()
self.maxsize = maxsize</p><pre class="brush:php;toolbar:false;">def get(self, key):
if key not in self.cache:
return None
self.cache.move_to_end(key) # 移到末尾表示最近使用
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
elif len(self.cache) >= self.maxsize > 0:
self.cache.popitem(last=False) # 删除最久未用(开头)
self.cache[key] = value
- 注意
maxsize=0表示无限制,但OrderedDict仍会记录插入顺序;而lru_cache(maxsize=0)实际禁用缓存 - 手动实现的 LRU 在并发读写时需加锁,
lru_cache内部已用线程锁保护,这点别忽略
LFU 必须额外维护频次,不能只靠顺序
LFU 要淘汰访问次数最少、且在次数相同时最久未用的项。这意味着至少要存两个维度:频次 + 最近访问时间。一种轻量做法是用 defaultdict(int) 记频次,再用 OrderedDict 按“频次+时间”复合排序——但 Python 没法动态重排 OrderedDict 键顺序,所以更稳的方式是用列表或堆。
简单可运行的 LFU 示例(非生产级,但逻辑清晰):
from collections import defaultdict, OrderedDict
<p>class LFUCache:
def <strong>init</strong>(self, maxsize=128):
self.cache = {} # key → value
self.freq = defaultdict(int) # key → count
self.freq_keys = defaultdict(OrderedDict) # freq → {key: None},按访问时间序
self.min_freq = 0
self.maxsize = maxsize</p><pre class="brush:php;toolbar:false;">def get(self, key):
if key not in self.cache:
return None
freq = self.freq[key]
# 从旧频次桶中移除
del self.freq_keys[freq][key]
if not self.freq_keys[freq]:
del self.freq_keys[freq]
if self.min_freq == freq:
self.min_freq += 1
# 频次+1,加入新桶末尾(表示最新访问)
self.freq[key] += 1
self.freq_keys[self.freq[key]][key] = None
return self.cache[key]
def put(self, key, value):
if self.maxsize = self.maxsize:
# 删除 min_freq 桶中最久未用的 key(OrderedDict 头部)
old_key = next(iter(self.freq_keys[self.min_freq]))
del self.cache[old_key]
del self.freq[old_key]
del self.freq_keys[self.min_freq][old_key]
if not self.freq_keys[self.min_freq]:
del self.freq_keys[self.min_freq]
self.cache[key] = value
self.freq[key] = 1
self.freq_keys[1][key] = None
self.min_freq = 1
这里的关键点是:freq_keys 每个频次对应一个 OrderedDict,保证同频次下按访问时间淘汰最老的;min_freq 动态跟踪当前最低频次,避免每次扫描全量。
性能与真实场景选择建议
LRU 实现简单、常数时间操作,适合大多数局部性明显(比如连续请求同一组 ID)的场景;LFU 在访问分布不均(少数热点 key 占大部分流量)时更省内存,但维护频次开销略高,且冷启动阶段容易误判——刚进缓存的 key 频次为 1,可能立刻被高频 key 挤出,哪怕它刚被访问过。
-
lru_cache默认线程安全,但自定义类如上需自行加threading.Lock - Python 3.12+ 的
functools.cache是lru_cache(maxsize=None)的别名,仍不支持 LFU - 真要 LFU 且要求高性能,建议用
pylru或cachetools.LFUCache,它们做过 C 层优化和边界 case 处理
缓存策略不是越复杂越好,先用 lru_cache 加 cache_info() 观察命中率,如果长期低于 60%,再考虑换 LFU 或调大 maxsize——很多时候问题不在算法,而在 key 设计或数据倾斜。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











