Python提升程序性能:深入函数缓存详解

发布时间:2023年12月17日

300?wx_fmt=png&wxfrom=19


概要

缓存是一种将计算结果临时存储起来的技术,以便在后续相同或类似的请求中直接使用该结果。缓存可以存储在内存、磁盘或其他介质上,以提高系统的性能和响应速度。缓存的工作原理是将计算结果与对应的输入参数关联起来,并存储在缓存中。当下次使用相同的参数进行计算时,首先检查缓存中是否存在对应的结果,如果存在,则直接返回缓存中的结果,而不必重新计算。


使用缓存可以提高系统性能和响应速度,减少计算资源的消耗。缓存适用于以下场景:

  • 计算结果具有重复性,即相同的输入参数会产生相同的结果。

  • 计算结果的获取代价较高,例如涉及网络请求数据库查询等耗时操作。

1. 引入

首先来看一个例子,也就是定义了一个求斐波那契数列的函数,包含了递归的使用。

def?fib(n):
????if?n?<=?1:??#?0?or?1
????????return?n
????return?fib(n?-?1)?+?fib(n?-?2)??

由于涉及到重复计算,这个递归函数在 n 大了以后会非常慢。,所以我们在想有没有什么办法可以降低重复运算,那么缓存就是一个不错的方法。

下边就来写一个缓存装饰器来优化它。传统方法是用个数组记录之前计算过的值,但是这种方式不够?Pythonic

from?functools?import?wraps

def?cache(func):
????"""先引入一个简单的装饰器缓存,其实原理很简单,就是内部用一个字典缓存已经计算过的结果"""
????store?=?{}

????@wraps(func)
????def?_(n):??#?这里函数没啥意义就随便用下划线命名了
????????if?n?in?store:
????????????return?store[n]
????????else:
????????????res?=?func(n)
????????????store[n]?=?res
????????????return?res

????return?_


@cache
def?f(n):
????if?n?<=?1:??#?0?or?1
????????return?n
????return?f(n?-?1)?+?f(n?-?2)

2. 装饰器实现缓存

functools.cache是在Python 3.2版本中引入的,用于缓存函数的结果。而from functools import lru_cache是在Python 3.9版本中引入的,用于实现最近最少使用(LRU)缓存策略,允许我们将一个函数的返回值快速地缓存或取消缓存。

  • functools.cache的使用方法如下:

import?functools

@functools.cache
def?fib(n):
????if?n?<?2:
????????return?n
????return?fib(n-1)?+?fib(n-2)

print(fib(10))??#?输出55

  • lru_cache的使用方法如下:

from?functools?import?lru_cache

@lru_cache(maxsize=32)
def?fib(n):
????if?n?<?2:
?????return?n
????return?fib(n-1)?+?fib(n-2)



print([fib(n)?for?n?in?range(10)])

#?Output:?[0,?1,?1,?2,?3,?5,?8,?13,?21,?34]


二者的区别:

  1. functools.cache是一个简单的装饰器,用于缓存函数的结果。它不会限制缓存的大小,当缓存满时,会抛出ValueError异常。

  2. lru_cache是一个装饰器,实现了最近最少使用(LRU)缓存策略。它可以设置缓存的最大大小,当缓存满时,会自动删除最近最少使用的缓存项。如果不设置最大大小,缓存可以无限制地增长。

  3. 那个?maxsize?参数是告诉?lru_cache,最多缓存最近多少个返回值。

注意:虽然cache函数可以提高函数的执行效率,但同时也会增加内存的消耗,因为缓存会占用一定的内存空间。因此,在应用cache函数时需要根据具体情况进行权衡和优化,避免过度使用导致内存占用过高。

另外,需要注意的是,使用cache函数缓存的结果会被保存在内存中,并且是永久性的,因此如果使用cache函数缓存了一些不再需要的结果,这些结果就会一直占用内存。为了避免这种情况,可以手动清除缓存。我们也可以轻松地对返回值清空缓存,通过这样:

fib.cache_clear()

3. 函数缓存拓展

函数缓存允许我们将一个函数对于给定参数的返回值缓存起来。当一个?I/O?密集的函数被频繁使用相同的参数调用的时候,函数缓存可以节约时间。常见的函数缓存实现方式包括字典缓存、LRU(Least Recently Used)缓存、Trie树等。不同的缓存方式适用于不同的情况,可以根据具体需求选择合适的缓存策略。

下面给出两个函数缓存示例:

  • 斐波那契数列计算:

def?fibonacci(n):
????if?n?<=?1:
????????return?n
????elif?n?not?in?cache:
????????cache[n]?=?fibonacci(n?-?1)?+?fibonacci(n?-?2)
????return?cache[n]

cache?=?{}
print(fibonacci(10))??#?输出55

在这个示例中,我们定义了一个名为fibonacci的函数来计算斐波那契数列的第n项。为了实现函数缓存,我们使用了一个名为cache的字典来存储已经计算好的斐波那契数列的值。当调用fibonacci函数时,首先检查输入参数是否已经在缓存中存在,如果存在则直接返回对应的值;否则,进行递归计算并将结果存入缓存中。最后,我们可以通过调用fibonacci(10)来测试我们的函数缓存实现。

  • 字符串匹配算法:

def?string_match(text,?pattern):
????if?pattern?==?"":
????????return?0
????elif?text?==?"":
????????return?-1
????elif?pattern[0]?==?text[0]:
????????return?1?+?string_match(text[1:],?pattern[1:])
????else:
????????result?=?string_match(text[1:],?pattern)
????????if?result?!=?-1:
????????????return?result?+?1
????????else:
????????????return?string_match(text,?pattern[1:])

print(string_match("hello?world",?"world"))??#?输出6

在这个示例中,我们定义了一个名为string_match的函数来实现字符串匹配算法。为了实现函数缓存,我们可以使用一个字典来存储已经计算过的子问题的结果。当遇到相同的子问题时,可以直接从缓存中获取结果,而不需要重新计算。这样可以减少重复计算,提高算法的效率。

注意事项:

  • 需要合理设置缓存的大小和过期策略,以避免过多的内存占用和缓存失效导致性能下降。

  • 需要注意并发访问的问题,可以使用锁或其他同步机制来保证线程安全。

  • 对于一些有副作用的函数或依赖于外部状态的函数,不适合使用函数缓存。

函数缓存虽然可以提高程序的性能,但也存在一些注意事项。首先,需要合理设置缓存的大小和过期策略,以避免过多的内存占用和缓存失效导致性能下降。其次,需要注意并发访问的问题,可以使用锁或其他同步机制来保证线程安全。此外,对于一些有副作用的函数或依赖于外部状态的函数,不适合使用函数缓存。

4.总结

综上所述,函数缓存是一种常用的优化技术,可以显著提高程序的性能。通过合理选择缓存方式和注意相关注意事项,我们可以更好地利用函数缓存来提升代码的效率和执行速度。

文章来源:https://blog.csdn.net/Rocky006/article/details/134868827
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。