Python中lru_cache的使用和实现详解

1. lru_cache的概念

lru_cache是Python 3.2版本中引入的一种函数缓存机制,用于提高函数的执行效率。lru_cache是Least Recently Used的缩写,翻译为“最近最少使用”。这种机制会缓存函数的输入和输出,当函数再次调用时,如果输入已经缓存过,函数就会直接返回输出,而不是重新计算。在缓存达到设定的最大值之前,缓存机制优先删除最少使用的缓存。

2. lru_cache的用法

2.1 基本用法

lru_cache的基本用法非常简单,在要缓存的函数上加上lru_cache的装饰器即可。

from functools import lru_cache

@lru_cache(maxsize=None)

def fibonacci(n):

if n < 2:

return n

return fibonacci(n-1) + fibonacci(n-2)

在上面的例子中,我们定义了一个递归计算斐波那契数列的函数,使用lru_cache装饰器可以提高函数的执行效率。maxsize参数指定了缓存的最大大小,如果设为None,则表示不限制缓存大小。

2.2 参数限制

lru_cache缓存的是函数的返回值,因此必须保证函数参数的可哈希性,否则会报错。比如,如果函数的参数是列表或字典,则会报错。

如果想要缓存不可哈希的参数,可以使用functools包中的lru_cache(maxsize=None, typed=True),其中typed参数表示对不同类型参数分别缓存。举个例子:

@lru_cache(maxsize=None, typed=True)

def expensive_func(a, b, c=None):

# do something expensive

pass

如果我们调用expensive_func(1, 2, c='foo'),然后再调用expensive_func(1, 2, c='bar'),由于参数c的类型不同,lru_cache会把它们作为不同的缓存。

3. lru_cache的实现原理

lru_cache并没有使用Python内置的字典或集合,而是利用了functools模块中提供的一个类——_lru_cache_wrapper。

3.1 _lru_cache_wrapper类

_lru_cache_wrapper是lru_cache的核心实现类,它继承自内置的SimpleNamespace类,主要包含以下属性:

cache_info(): 返回缓存信息,包括缓存的大小和命中次数。

cache_clear(): 清除缓存。

maxsize: 缓存的最大大小,如果是None,则表示不限制缓存大小。

typed: 是否对不同类型的参数分别缓存。

当我们在函数上使用lru_cache装饰器时,实际上是创建了一个_lru_cache_wrapper对象,并将函数作为参数传递给它的构造函数。

3.2 缓存机制的实现

lru_cache的缓存机制主要是通过一个双向链表和一个字典来实现的。字典用于存储参数和结果的映射关系,而双向链表则用于记录参数的使用顺序。

代码实现如下:

class LRUCache(OrderedDict):

def __init__(self, maxsize=None):

if not isinstance(maxsize, int) or maxsize <= 0:

maxsize = None

self.maxsize = maxsize

self.cache_info = lambda: {'size': len(self), 'maxsize': self.maxsize}

self.hits = self.misses = 0

def __getitem__(self, key):

if key in self:

self.move_to_end(key)

self.hits += 1

return self[key]

self.misses += 1

return None

def __setitem__(self, key, value):

self.move_to_end(key)

super().__setitem__(key, value)

if self.maxsize is not None and len(self) > self.maxsize:

oldest = next(iter(self))

del self[oldest]

def __repr__(self):

return f'LRUCache({self.maxsize}, {list(self.items())})'

在LRUCache类中,我们继承了内置的OrderedDict类,并重写了它的几个方法:

__init__(): 初始化缓存大小和命中次数与未命中次数。

__getitem__(): 如果key已经在缓存中,将其移动到队列尾部,增加hits计数器,并返回其对应的值;否则增加misses计数器,并返回None。

__setitem__(): 如果key已经在缓存中,将其移动到队列尾部,并更新其对应的值;否则将其插入缓存尾部,并判断缓存大小是否超过了设定的最大值,如果超过了,删除最旧的缓存数据。

__repr__(): 返回缓存信息,包括缓存大小和内容。

通过以上实现,我们就可以通过Python代码了解lru_cache的原理了。

4. 使用lru_cache的注意事项

虽然lru_cache是一个非常实用的工具,但是在使用时也有一些需要注意的问题。

4.1 命中率计算的问题

需要注意的是,lru_cache中的命中率计算方式可能会出现误差。lru_cache本身会记录缓存的命中次数和未命中次数,并提供cache_info方法来查看。但是,由于Python的解释器和操作系统的调度机制等问题,Python函数的执行时间可能会受到各种因素的影响,导致同样的代码在不同的时间下命中率不一定相同。

4.2 参数的可哈希性

在使用lru_cache时,需要保证函数的参数是可哈希的。如果参数是不可哈希的,可以使用functools.lru_cache(maxsize=None, typed=True)的typed参数来解决。但是,需要注意的是,由于使用了缓存机制,缓存的过程会对内存造成一定的压力,所以在缓存数量较大时,需要小心内存使用情况。

4.3 缓存的并发问题

lru_cache本身并不保证线程安全,如果多个线程同时访问同一函数,在缓存没有达到最大值时,可能会导致命中率计算的异常,或者缓存的数据出现问题。如果需要实现缓存的并发控制,可以使用concurrent.futures模块中的ThreadPoolExecutor或ProcessPoolExecutor来控制线程或进程,或者使用Python自带的线程锁或进程锁来控制并发访问。

5. 总结

lru_cache是Python 3.2版本中引入的一种函数缓存机制,用于提高函数的执行效率。它的基本用法非常简单,在要缓存的函数上加上lru_cache装饰器即可。lru_cache的实现主要是通过一个双向链表和一个字典来实现的。同时,在使用lru_cache时需要注意一些问题,比如命中率计算的问题、参数的可哈希性和缓存的并发问题。

后端开发标签