让lru_cache忽略某些参数

  |  

摘要: Python 缓存策略工具

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


在文章 Python标准库-lru_cache缓存 中,我们介绍了 lru_cache 的用法。例如有以下函数:

1
2
@lru_cache
def solve(i, j, handle)

按照 lru_cache 的逻辑,ijhandle 都是缓存键,当我们调用此前调用过的 (i, j) 对,但 handle 不一样,那么 solve 依然会再次执行。但当 handle 的不同取值对结果没有影响的时候我们就想避免这种情况,也就是说让 lru_cache 仅把 i, j 视为缓存键,而忽略 handle 这个参数。

一种方法是用装饰器实现,另一种是使用 cachetools。

让 lru_cache 忽略某些参数的装饰器

一种做法是用装饰器实现,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
def ignore_args(*args_to_ignore):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
new_args = [arg for arg in args if arg not in args_to_ignore]
return func(*new_args, **kwargs)
return wrapper
return decorator

@ignore_args("handle")
@lru_cache
def solve(i, j, handle)

在代码中,我们定义了一个装饰器函数 ignore_args,它接收需要忽略的参数名,例如 handle,然后返回一个装饰器函数。

在装饰器函数 decorator 接收一个函数 func,然后定义了一个新的函数 wrapper,在 wrapper 中,通过 @wraps 装饰器来保留被装饰函数的元信息

这里的 wraps 函数是一个不好理解的特性,在文章 Python标准库-包装Callable对象 中有提到过。

Python装饰器在装饰其他函数的的时候,被装饰后的函数的函数名等属性会发生改变。
为了避免这种情况的发生,可以用 functools.wraps 装饰器来消除这样的副作用。在定义装饰器的时候,可以在装饰器的内函数之前加上 wraps,这样它就能保留被装饰函数的名称和函数属性。
装饰器函数里面的函数在被 wraps 装饰后,此时再装饰其他函数时,被装饰函数的函数名、文档字符串等函数属性就不会再受到影响。

在wrapper函数中,我们根据传入的参数列表 args 和关键字参数 kwargs,创建了一个新的参数列表 new_args,在这个列表中,我们仅保留了不属于 args_to_ignore 的参数。

最后,我们使用 func(*new_args, **kwargs) 来调用原始函数,忽略了被忽略的参数。

这个代码模板在 树的最大权独立集:树形DP 中有用到,可以参考。

cachetools 简介

cachetools 是一个 Python 缓存工具库,用于缓存方法的返回值。其中提供了各种缓存策略和数据结构,主要是以下 5 个:

  • Cached
  • LRUCache
  • TTLCache
  • LFUCache
  • RRCache

cached

cached 是一个装饰器,默认情况下,它会执行一个简单缓存。

1
2
3
4
5
6
7
8
9
from cachetools import cached 

@cached(cache={})
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(1000))

LRUCache

LRUCache 在缓存装饰器 cached 内部使用,接收一个 maxsize 参数。完成的工程与我们常用的 functools.lru_cache 相同。

1
2
3
4
5
6
7
8
9
from cachetools import cached, LRUCache

@cached(cache=LRUCache(maxsize=10000))
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(1000))

TTLCache

TTLCache 缓存有两个参数,maxsize 的使用与 LRUCache 相同,TTL 值表示缓存应存储多长时间。该值以秒为单位。

1
2
3
4
5
6
7
8
9
from cachetools import cached, TTLCache

@cached(cache=TTLCache(maxsize=10000, ttl=25))
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(1000))

LFUCache

LFUCache 缓存是另一种类型的缓存技术,用于检索项目被调用的频率,它会在必要时丢弃最不常调用的项目以腾出空间。LFUCache 接收一个参数 maxsize,与 LRUCache 中的相同。

1
2
3
4
5
6
7
8
9
from cachetools import cached, LFUCache

@cached(cache=LFUCache(maxsize=10000))
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(1000))

RRCache

RRCache 缓存是另一种缓存技术,它随机选择缓存中的项目并在必要时丢弃它们以释放空间。同样接收一个参数 maxsize,与 LRUCache 中的相同。

1
2
3
4
5
6
7
8
9
from cachetools import cached, RRCache

@cached(cache=RRCache(maxsize=10000))
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(1000))

用 cachetools 忽略一些缓存键

示例代码如下,在 key=lambda n, handle: hashkey(n) 中说明,参数有 n, handle,但只以 n 作为缓存键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from cachetools import cached
from cachetools.keys import hashkey

from random import randint

@cached(cache={}, key=lambda n, handle: hashkey(n))
def fibonacci(n, handle):
print("handle: {}, n: {}".format(handle, n))
if n < 2:
return n
return fibonacci(n-1, handle) + fibonacci(n-2, handle)

print(fibonacci(10, "xyz"))
print(fibonacci(20, "abc"))

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
handle: xyz, n: 10
handle: xyz, n: 9
handle: xyz, n: 8
handle: xyz, n: 7
handle: xyz, n: 6
handle: xyz, n: 5
handle: xyz, n: 4
handle: xyz, n: 3
handle: xyz, n: 2
handle: xyz, n: 1
handle: xyz, n: 0
55
handle: abc, n: 20
handle: abc, n: 19
handle: abc, n: 18
handle: abc, n: 17
handle: abc, n: 16
handle: abc, n: 15
handle: abc, n: 14
handle: abc, n: 13
handle: abc, n: 12
handle: abc, n: 11
6765

可以看到,第二次调用 fibonacci(20, "abc") 的时候,其中 0 ~ 10 的部分,即使 handle 不一样,也没有运行函数。


Share