本文将和你一同开发一个缓存装饰器,届时,你将了解缓存的一般算法,对于编写装饰器也更加得心应手
经典的缓存算法有3个:
FIFO(First in First out),先进先出, 该算法的核心原则是: 如果一个数据最先进入缓存中,则应该最早淘汰掉,当缓存容量满了以后,应当将最早被缓存的数据淘汰掉。FIFO算法是一种比较简单的算法,使用队列就可以轻易的实现。
LFU(Least Frequently Used)最近最少使用算法, 这个算法的核心在于:如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
LRU (Least Recently Used), 最近最久未使用算法,该算法的核心原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小
LFU算法和LRU算法乍看起来是一个意思,但其实很不同,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。
一个缓存的数据,一段时间内被命中很多次,这个数据在LFU算法里会被保留,但在LRU算法里则可能被淘汰,虽然这段时间内,比如2分钟内被命中了很多次,可是,这些事情都发生在1分50秒之前的10秒钟里,自那以后就再也没有被命中,LRU算法则可能会将其淘汰。
3种算法里,最容易实现的是FIFO算法,因此这个装饰器使用FIFO算法来实现。
使用一个列表来保存函数的参数,并且规定这个列表的最大长度,缓存不能无限增加。使用一个字典,以参数做key, 以函数返回结果做value。
from functools import wraps
from inspect import signature
def fifo_cache(maxsize=128):
lst = []
cache = {}
def wrapper(func):
sig = signature(func) # 获得参数列表
@wraps(func)
def inner(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs) # 绑定参数
# print(bound_values)
key = bound_values.__str__()
value = cache.get(key)
if value is not None:
print('命中缓存')
return value
if len(lst) >= maxsize:
oldkey = lst.pop(0)
if oldkey in cache:
cache.pop(oldkey)
result = func(*args, **kwargs)
lst.append(key)
cache[key] = result
return result
return inner
return wrapper
@fifo_cache()
def test1(x, y):
return x + y
@fifo_cache()
def test2(x, y, z=20):
return x + y + z
@fifo_cache()
def test3(*args, **kwargs):
return 5
print(test1(19, 20))
print(test2(19, 20, 20))
print(test2(19, 20)) # 不会命中缓存
print(test3(4, 2, x=6, y=9))
print(test1(19, 20))
代码执行输出结果
39
59
59
5
命中缓存
39
在第二次调用test1(19, 20)时,命中了缓存
使用inspect的signature函数可以获取函数定义的参数的顺序以及函数注解
signature返回的sig是类Signature的实例对象,该对象的bind方法返回一个BoundArguments对象,下面是其中一个示例
<BoundArguments (args=(4, 2), kwargs={'x': 6, 'y': 9})>
方法在实际传入的参数和函数的参数列表之间建立了一个映射关系
这个装饰器还存在着一个小的缺陷,个别情况下无法命中缓存,函数test2的最后一个参数z是默认参数,默认值是20, test2(19, 20, 20) 与 test2(19, 20) 本质上传入的参数是相同的,但在inner函数里接收到的参数里没有默认参数的数值,这里一定有办法可以解决这个问题,暂且搁置,待我慢慢研究。
QQ交流群: 211426309