不能直接用move_to_end()因旧版python缺失该方法,且仅靠它易漏更新访问顺序;须重载__getitem__和__setitem__,统一用pop()+__setitem__实现“先删后插”,并用popitem(last=false)淘汰最老项。

为什么不能直接用 collections.OrderedDict.move_to_end()
因为 move_to_end() 是 Python 3.2+ 才加的,如果你在旧环境(比如某些嵌入式 Python 或冻结的容器镜像)里跑,它会抛 AttributeError: 'OrderedDict' object has no attribute 'move_to_end'。更麻烦的是,哪怕有这个方法,单纯靠它模拟 LRU 容易漏掉「访问即更新顺序」这个关键点——比如你查了 key 但没调用 move_to_end(),那这条记录就卡在老位置,下次淘汰时可能误删活跃项。
- 必须在
__getitem__和__setitem__里显式触发顺序更新 - 如果只重载
__setitem__,读操作不会刷新顺序,缓存行为就不是真正的 LRU - Python 2.7 的
OrderedDict没有move_to_end(),得用pop(key)+__setitem__(key, value)组合来模拟
如何手写一个兼容 Python 2.7–3.11 的 LRU_OrderedDict
核心是把「插入/更新」和「访问」都统一成「先删后插」逻辑,不依赖 move_to_end()。这样既绕过版本限制,又保证语义正确。
- 继承
collections.OrderedDict,重载__getitem__:先self.pop(key),再super().__setitem__(key, value) - 重载
__setitem__:如果 key 已存在,先self.pop(key),再设值;否则直接设 - 淘汰逻辑放在
__setitem__末尾:当len(self) > self.maxsize时,self.popitem(last=False)(踢最老的) -
maxsize建议设为正整数,设为None时跳过淘汰判断,避免每次写都做长度检查
class LRU_OrderedDict(collections.OrderedDict):
def __init__(self, maxsize=128):
super().__init__()
self.maxsize = maxsize
def __getitem__(self, key):
try:
value = super().__getitem__(key)
except KeyError:
raise
else:
self.pop(key)
super().__setitem__(key, value)
return value
def __setitem__(self, key, value):
if key in self:
self.pop(key)
super().__setitem__(key, value)
if self.maxsize is not None and len(self) > self.maxsize:
self.popitem(last=False)
popitem(last=False) 和 popitem(last=True) 的性能差异
last=False 是 O(1),last=True 也是 O(1),但底层实现不同:前者弹出头节点(最老),后者弹出尾节点(最新)。LRU 必须用 last=False,否则淘汰的就是刚访问过的热数据。
- 别被名字误导:
last=False≠ “不弹最后”,而是“弹第一个”(OrderedDict 内部是双向链表,头是最老) - 如果误用
last=True,缓存会退化成 MRU(Most Recently Used),热点数据反而最早被淘汰 - 在 CPython 3.7+ 的 dict 里,
popitem()默认行为变了,但OrderedDict始终保持兼容,所以必须显式写last=False
为什么不用 functools.lru_cache 替代?
因为 lru_cache 是装饰器,绑定在函数上,没法手动增删 key、没法查当前 size、没法共享缓存实例,也不支持非 hashable 参数(比如 list 或 dict 当参数时直接报 TypeError)。而 LRU_OrderedDict 是裸数据结构,你可以把它塞进类属性、传给其他模块、甚至序列化部分状态。
立即学习“Python免费学习笔记(深入)”;
- 需要动态控制缓存生命周期?
lru_cache的cache_clear()是全局清空,没法按 key 清 - 要监控命中率?
LRU_OrderedDict可以加计数器,lru_cache的cache_info()只返回统计,不暴露内部结构 - 参数含可变对象?
lru_cache直接拒绝,LRU_OrderedDict由你决定怎么序列化 key(比如用json.dumps(sorted(items.items())))
真正难的不是写对逻辑,是记住每次读都要触发顺序更新——漏一次,整个缓存就偏了。很多人在 __getitem__ 里只取值不更新,测的时候看不出问题,压测一跑,冷热混杂,命中率断崖下跌。










