python使用装饰器实现缓存

本文将和你一同开发一个缓存装饰器,届时,你将了解缓存的一般算法,对于编写装饰器也更加得心应手

1. 缓存算法

经典的缓存算法有3个:

  1. FIFO算法
  2. LFU算法
  3. LRU算法

1.1 FIFO算法

FIFO(First in First out),先进先出, 该算法的核心原则是: 如果一个数据最先进入缓存中,则应该最早淘汰掉,当缓存容量满了以后,应当将最早被缓存的数据淘汰掉。FIFO算法是一种比较简单的算法,使用队列就可以轻易的实现。

1.2 LFU算法

LFU(Least Frequently Used)最近最少使用算法, 这个算法的核心在于:如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小

1.3 LRU算法

LRU (Least Recently Used), 最近最久未使用算法,该算法的核心原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小

LFU算法和LRU算法乍看起来是一个意思,但其实很不同,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。

一个缓存的数据,一段时间内被命中很多次,这个数据在LFU算法里会被保留,但在LRU算法里则可能被淘汰,虽然这段时间内,比如2分钟内被命中了很多次,可是,这些事情都发生在1分50秒之前的10秒钟里,自那以后就再也没有被命中,LRU算法则可能会将其淘汰。

2. 装饰器实现

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

使用inspect的signature函数可以获取函数定义的参数的顺序以及函数注解

sig.bind

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

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案