LRU Cache在Python中的实现
LRU Cache - Least Recently Used Cache
最近最久未使用缓存
今天问了同事一个问题,LRU Cache系统如何实现,同事答使用时间戳。那么使用时间戳的话,他可能的想法是在Python里的字典中实现,这样通过判断时间戳的迟早来实现LRU。
首先缓存,比如对于一个这样的函数,task(arg1, arg2)
,缓存主要是缓存其参数和返回值,那么就可以直接使用字典来实现,关键在于参数是可以哈希的,也就是说参数不应该是可变的,这样就可以将参数作为key,函数返回值作为value来存储到字典之中。
那么对于LRU,LRU比较常用的在操作系统内存管理的页面置换中使用到了,就是最近使用的页面在内存中,否则就在硬盘里,为了提高页面的命中率,其英文为hits,没有命中则为misses。对于上面的这个函数来说就是要存储最近使用的参数和返回值这么一个对应关系。
时间戳字典可行吗?
对于同事提到的字典,其缓存模型应该是函数参数作为key,因为要通过key来判断是否命中,时间戳也作为key,因为要比较最近使用,比如缓存已经满了,要将最早使用的缓存清除出去,那么这时候就要比较哪个参数对应的时间是最早的。
这里有一种multi_key_dict
,也就是多个键对应一个值,假设时间戳不会冲突的话,使用时间戳和参数作为键,那么要取到最早的时间戳,也需要一个排序,因为键在字典中是无序的。且不说从这么多键中找出时间戳这种键,并且还有一个问题是如果参数也是时间戳呢= = !
使用LIST
既然要取时间戳需要排序,那么不用时间戳,直接将对应的key也就是函数参数存储到一个Python列表中就行了,最近使用的在列表头,最早使用的在列表尾,然后用一个字典存储参数-返回值,这样就实现了一个LRU嘛。
如果有参数命中(key在字典中),那么就把这个参数从其原始位置放到列表头,如果满了就删除最后一个元素。这样看起来可以,但是涉及性能问题。从列表中间pop和从头insert都是性能瓶颈操作,时间复杂度在O(n)
。
使用链表
既然使用list在insert时候有性能问题,那么使用单链表呢,只有有头指针,我就可以在头部插入,此时时间复杂度只有O(1)
。但是将参数从单链表尾部移除呢,还得从头遍历一遍到尾部,那那那就用双向循环链表好了,这样找尾部元素,就可以直接使用头指针向前得到了。
看起来好像一切OK了,但是认真想想,如果命中一个参数,那么要将其从中间某个位置弄到最开头呢?双向循环链表还是得去遍历一遍链表去查找,平均的时间复杂度也要O(n)
使用字典和链表相结合
在取缓存的时候,通过字典直接从参数取到返回值,那么可不可以将链表和字典结合起来呢,使用key能不能直接获取到key在链表的位置呢?当然可以,key对应的value存储为这个key对应的链表节点不就可以了,同时这个链表节点还存储了返回值不就刚好可以将返回值取到了嘛。
FUNCTOOLS.LRU_CACHE
Python中functools的lru_cache
实现了LRU Cache,核心是一个双向循环链表再加一个字典。
from functools import lru_cache
可以看到lru_cache
的代码:
_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
def lru_cache(maxsize=128, typed=False):
'''最近最少使用的缓存装饰器
如果maxsize为None,则不使用LRU特性,并且缓存可以没有限制的增长
如果typed为True,不同类型的参数将会独立的缓存,比如f(3.0)和f(3)将被看作不同的调用,并且有不同的结果。
缓存函数的参数必须是可哈希的。
访问命名元组(hints, misses, maxsize, currsize)来查看缓存统计数据,使用f.cache_info()来得到。使用f.cache_clear()来清除缓存数据。使用f.__wrapped__来获取底层包装函数
'''
if maxsize is not None and not isinstance(maxsize, int):
raise TypeError('Expected maxsize to be an integer or None')
def decorating_function(user_function):
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
return update_wrapper(wrapper, user_function)
return decorating_function
首先这是一个带参数的装饰器,使用的时候应该@lru_cache()
,这样会返回decorating_function
,用来装饰用户的函数,其内部会返回将用户的包装后的函数,使用的是_lru_cache_wrapper
:
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
# 所有lru缓存实例共享的常量
sentinel = object() # 当缓存没有命中的时候返回的对象
make_key = _make_key # 从函数参数来生成一个key的函数
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # 每一个链表节点中存储的每个元素位置
cache = {}
hits = misses = 0
full = False
cache_get = cache.get # 绑定查询键的函数
lock = RLock() # 链表的更新可能不是线程安全的,对于一个函数,可能多线程调用
root = [] # 双向循环链表的头指针
root[:] = [root, root, None, None] # 初始化双向循环链表
if maxsize == 0:
# 不使用缓存,每次调用没有命中次数都会加一
def wrapper(*args, **kwds):
# 重新绑定自由变量misses
nonlocal misses
result = user_function(*args, **kwds)
misses += 1
return result
elif maxsize is None:
# 简单的缓存,没有次序,也没有大小限制,也就是直接使用字典进行缓存
def wrapper(*args, **kwds):
nonlocal hits, misses
key = make_key(args, kwds, typed)
result = cache_get(key, sentinel)
if result is not sentinel:
hits += 1
return result
result = user_function(*args, **kwds)
cache[key] = result
misses += 1
return result
else:
# 这种情况就是有大小限制的lru_cache,我觉得应该加一个有限制的普通缓存系统
def wrapper(*args, **kwds):
nonlocal root, hits, misses, full
key = make_key(args, kwds, typed)
with lock:
link = cache_get(key)
# 如果命中,将命中的链表节点放到头部,每个链表节点存储了PREV,NEXT,KEY,VALUE
# 所以先将当前节点的前后节点接在一起
if link is not None:
# 从链表中取出这个节点
link_prev, link_next, _key, result = link
link_prev[NEXT] = link_next
link_next[PREV] = link_prev
# 将链表节点放到头部,root前面
last = root[PREV]
last[NEXT] = root[PREV] = link
link[PREV] = last
link[NEXT] = root]
hits += 1
return result
result = user_function(*args, **kwds)
# 没有命中的话,将其放入缓存,更新操作必须加锁
with lock:
if key in cache:
# 如果key已经在cache,说明有其它的线程已经将这个key放入缓存中了
pass
elif full:
# 如果缓存已经满了,则取出最久没有使用的节点将其pop出去,也就是双向循环链表中root后面的指针。
# 这段代码的具体过程是,将root指针作为新节点,root指针后面的节点作为root节点
oldroot = root
oldroot[KEY] = key
oldroot[RESULT] = result
# root的节点作为新节点,只需要将key,result换掉
root = oldroot[NEXT]
# 记录oldkey是为了在cache字典中删除
oldkey = root[KEY]
oldresult = root[RESULT]
root[KEY] = root[RESULT] = None
# 更新好root节点后删除字典中的key
del cache[oldkey]
cache[key] = oldroot
else:
# 如果缓存没有满,更新缓存
last = root[PREV]
# 新的链表节点在root和root前一个节点之间
link = [last, root, key, result]
last[NEXT] = root[PREV] = cache[key] = link
full = (len(cache) >= maxsize)
misses += 1
return result
def cache_info():
"""Report cache statistics"""
with lock:
# 因为以下数据可能在更新,所以加锁获取共享资源
return _CacheInfo(hits, misses, maxsize, len(cache))
def cache_clear():
"""Clear the cache and cache statistics"""
nonlocal hits, misses, full
with lock:
cache.clear()
root[:] = [root, root, None, None]
hits = misses = 0
full = False
wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
return wrapper
当缓存已经满了的时候,这种处理方式还是挺不错的,但是要是我来搞就是删除后面的节点,然后将新节点放到root前面。另外这个人是左撇子么,喜欢逆时针转啊。
ORDEREDDICT相关
另一个使用双向循环链表的例子是OrderedDict,链表保存了key添加到字典的顺序,但其实这个字典实际用的时候,是用的C语言实现的版本:
try:
from _collections import OrderedDict
except ImportError:
# Leave the pure Python version in place.
pass
下面是OrderedDict的源码:
class OrderedDict(dict):
'''记录插入顺序的字典
内部的self.__map字典记录了key到双向循环链表的节点,双向循环链表开始和结束都是一个sentinel元素。这个元素从来都不会被删除,它在self.__hardroot, 并且self.__hardroot在self.__root有一个弱引用代理
往前的连接是弱引用代理,为了防止循环引用
在self.__map中存储了硬连接到单个链表节点上,当key被删除后,这些硬连接就会消失
'''
def __init__(*args, **kwds):
'''Initialize an ordered dictionary. The signature is the same as
regular dictionaries, but keyword arguments are not recommended because
their insertion order is arbitrary.
'''
if not args:
raise TypeError("descriptor '__init__' of 'OrderedDict' object "
"needs an argument")
self, *args = args
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__root
except AttributeError:
# 这里使用的是一个限定属性的类来作为链表节点
self.__hardroot = _Link()
self.__root = root = _proxy(self.__hardroot)
root.prev = root.next = root
self.__map = {}
self.__update(*args, **kwds)
这里_Link()
是一个限定属性的类,代码如下;
class _Link(object):
__slots__ = 'prev', 'next', 'key', '__weakref__'
_proxy
是from _weakref import proxy as _proxy
对一个对象的弱引用。相对于通常的引用来说,如果一个对象有一个常规的引用,它是不会被垃圾收集器销毁的,但是如果一个对象只剩下一个弱引用,那么它可能被垃圾收集器收回。werkref
def proxy(p_object, callback=None):
"""
创建一个代理对象,弱引用至object
"""
pass
继续来看OrderedDict:
def __setitem__(self, key, value, dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
# 当设置一个新值时,首先查看是否已经在字典中了,如果在字典中,就不需要再加入到链表中了,保持之前的次序
# 如果没有,则加入到链表中,同样是将新节点放到root前面一个
if key not in self:
self.__map[key] = link = Link()
root = self.__root
last = root.prev
link.prev, link.next, link.key = last, root, key
last.next = link
# prev连接是一个proxy
root.prev = proxy(link)
dict_setitem(self, key, value)
def __delitem__(self, key, dict_delitem=dict.__delitem__):
dict_delitem(self, key)
link = self.__map.pop(key)
link_prev = link.prev
link_next = link.next
link_prev.next = link_next
link_next.prev = link_prev
link.prev = None
link.next = None
def __iter__(self):
root = self.__root
curr = root.next
while curr is not root:
yield curr.key
curr = curr.next
def __reversed__(self):
root = self.__root
curr = root.prev
while curr is not root:
yield curr.key
curr = curr.prev
def clear(self):
root = self.__root
root.prev = root.next = root
self.__map.clear()
dict.clear(self)
其它方法就不一一说明了,但是从中可以看出,涉及到元素顺序的操作,双向循环链表的应用还是很广泛的。
相关推荐
总的来说,`backports.functools_lru_cache`库为旧版本的Python提供了`functools.lru_cache`功能,使得开发者能够在更广泛的环境中享受到缓存带来的性能提升。通过学习其源码,我们可以深入了解Python装饰器的实现...
内容概要:本篇文章详细介绍了双向链表的概念、结构及其实现方法,并附带了一个复杂的例子——最少最近使用缓存(LRU Cache)的具体实现实验案例,有助于理解该数据结构的运作方式及应用潜力。 适合人群:适用于对...
9. **Python实现**:在Python中,可以使用字典或其他数据结构来表示Cache的存储块,利用位运算来处理地址映射,以及编写逻辑来实现替换策略和写策略。 通过这个Python实现的Cache模拟项目,学习者可以亲手操作,...
在Python中,为了满足这些需求,我们可以使用一个字典来存储键值对,并结合一个双向链表来实现优先级队列。双向链表允许我们快速地插入、删除和移动节点,这对于LRU缓存的操作至关重要。 以下是一个简单的LRU缓存...
在Python中,我们可以使用不同的方式来实现LRU算法,主要介绍两种方法:使用`collections.OrderedDict`和使用`dict`与`list`的组合。 ### 方法一:使用`collections.OrderedDict` `collections.OrderedDict` 是...
python python_leetcode题解之146_LRU_Cache
python源码安装包backports.functools_lru_cache-1.5.tar 解压后python setup.py install 进行安装
from lru import lru_cache_function @ lru_cache_function ( max_size = 1024 , expiration = 15 * 60 ) def f ( x ): print "Calling f(" + str ( x ) + ")" return x f ( 3 ) # This will print "Calling f(3)...
在发布的Python 3.3中的functools.lru_cache的反向。 用法 考虑使用此技术导入“ lru_cache”函数: try: from functools import lru_cache except ImportError: from backports.functools_lru_cache import lru_...
Python_lru是一个在Python开发中常用的轻量级缓存库,它的全称为Least Recently Used,即最近最少使用。这个库的主要功能是实现LRU缓存策略,帮助开发者快速存储和检索数据,尤其适用于处理有限内存资源的情况。LRU...
实现对整个练习进行了总结,以证明“所有语言虽然可以具有相同的逻辑,但由于“最佳实践”和其他问题而可能具有不同的实现”,并且还让我赶上了所有语言我熟悉(对不起,python,仅限二进制生产者)。 代码非常详细...
在Python中,缓存是一种优化策略,用于存储计算结果或I/O操作,以便后续请求可以快速获取数据,而无需重复计算或等待I/O完成。这在处理大量数据或执行昂贵计算时非常有用,可以显著提升应用性能。 `diskcache`库的...
用于学习目的的Python中LRU缓存的简单实现。 数据结构 在此实现中,我们使用LRU缓存的经典版本,该版本是使用双链表和哈希映射实现的。 我们有一个自定义的双链表实现。 双链表 双链表的API如下: 姓名 描述 ...
在Python中实现LRU热点缓存可以帮助优化频繁访问的数据的处理速度,尤其在内存有限的情况下。 LRU缓存通常由两个主要部分组成:一个存储数据的容器(如列表)和一个用于快速查找数据的索引(如哈希表)。在Python的...
在现代编程语言中,例如Java和Python,都有内置的LRU缓存实现,简化了开发者的使用。 在分析和理解这个LRU算法demo时,你需要关注以下几个方面: 1. 如何实现数据项的添加和查找,以及如何维护访问顺序。 2. 如何...
在实际编程中,可以使用各种编程语言的库来实现LRU,例如Python的`functools.lru_cache`,Java的`LinkedHashMap`,或者是C++的自定义实现。 总之,LRU算法是一种高效的内存管理策略,通过优先淘汰最近最少使用的...
在Python中,我们可以利用内置的库和装饰器来实现缓存功能,其中functools模块中的lru_cache是一个非常实用的装饰器,它使用了LRU(Least Recently Used,最近最少使用)算法来缓存函数的调用结果。以下将详细介绍...
在Python中,实现LRU缓存机制可以借助于`collections`模块中的`OrderedDict`或者`deque`双端队列。本题解将深入探讨第146题的LRU缓存问题,以及如何使用Python来解决此类问题。 首先,我们来看题目的具体要求。...
3. **模拟器的实现**:可能涉及到使用编程语言(如C++、Python)来模拟Cache的行为,包括如何处理内存请求、如何更新Cache状态、如何执行替换策略等。 4. **实验设计**:可能包括不同Cache配置的比较,例如不同大小...