不用functools.lru_cache而要自己写,因其缓存生命周期绑定函数、无法手动清理或动态调容,且不支持异步;自实现可控制淘汰逻辑、嵌入实例、联动监控,初学者应先掌握节点连接、淘汰触发和O(1)查找。

为什么不用 functools.lru_cache 而要自己写
因为 functools.lru_cache 是装饰器,缓存生命周期绑定在函数对象上,无法手动清理、无法按条件驱逐、不支持异步函数,且缓存大小在装饰时就固定死——比如你想在运行时动态调大容量,或根据内存压力缩容,它做不到。
自己实现的核心价值是可控:能响应外部信号、可嵌入类实例、能和日志/监控联动。新手不必追求工业级(如 asyncio 支持或线程安全),先搞懂「节点怎么连」「淘汰怎么触发」「查找怎么 O(1)」这三个点就够了。
用 collections.OrderedDict 实现最简 LRU
OrderedDict 天然维护插入顺序,move_to_end(key) 把访问项移到末尾,popitem(last=False) 弹出最久未用项(即开头),这两步就覆盖了 LRU 的核心行为。
注意几个易错点:
- 必须在
__getitem__里调用move_to_end(key),否则读操作不会更新“最近使用”状态 - 写入时如果已存在 key,要先
pop(key)再重新setitem,否则OrderedDict不会自动移到末尾(Python 3.7+ 普通 dict 虽保持插入序,但没move_to_end) - 容量检查必须放在写入后做,不能在写入前——否则新 key 还没进 dict 就误删了旧项
示例骨架:
from collections import OrderedDict
<p>class LRUCache:
def <strong>init</strong>(self, capacity: int):
self.cache = OrderedDict()
self.capacity = capacity</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.pop(key) # 先删再插,确保顺序更新
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # 弹出最老项
手动实现双向链表 + 哈希表的动机与代价
当你需要严格 O(1) 时间复杂度、或者想彻底理解 LRU 底层逻辑时,才会跳过 OrderedDict 自己造轮子。Python 标准库的 OrderedDict 本身也是基于双向链表 + dict 实现的,但封装了细节。
自己写要处理的麻烦事:
- 每个节点得存
key、value、prev、next四个字段,容易写错指针指向 - 插入/删除时要同时更新哈希表(
dict)和链表,两处修改必须原子——Python 没锁也得靠逻辑保证不中断 - 边界 case 多:空链表、单节点、头尾操作……一不留神就
AttributeError: 'NoneType' object has no attribute 'next'
除非你正在学数据结构、或面试被要求手写,否则真没必要。生产环境更推荐用成熟的 lru-dict 包(C 扩展,比 OrderedDict 快)或直接上 redis。
缓存失效、类型安全与实际调试建议
新手常忽略缓存值本身的生命周期。比如缓存了一个列表,外部代码修改了这个列表内容,缓存里存的还是同一个对象引用——表面没变,实际已脏。
几个实用提醒:
- 缓存前考虑是否要
copy.deepcopy,尤其是可变对象;但 deepcopy 有性能开销,别无脑套用 - key 类型必须可哈希,
list、dict直接当 key 会报TypeError: unhashable type,得转成tuple或json.dumps字符串 - 加个
__len__和__repr__方法,调试时一眼看清当前缓存大小和内容,比反复 printlist(cache.keys())高效得多 - 用
sys.getsizeof看不出缓存总内存占用——它只算对象顶层,不递归算子对象。真要控内存,得结合pympler或限制 key/value 类型
LRU 的“简单”是相对的,真正卡住人的往往不是算法,而是缓存语义是否和业务一致:比如一个用户配置项,缓存 5 秒够不够?失效后是穿透 DB 还是返回旧值?这些得靠业务场景定,代码只是执行者。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











