1. 引言
谈到提高Python
程序的执行性能,尤其是数据处理性能,有太多的第三方库可以帮助我们。从它们的机制来看,大多数都是依靠优化数据结构或内存利用率来实现性能提升的。
事实上,有一种Python
原生缓存装饰器可以用来显著提高性能。我们不需要安装任何东西,因为它是Python
内置的。当然,它不会用于所有场景。因此,我们还将讨论什么情况下我们不应该使用它。
2. 举个栗子
让我们从一个大家都熟悉的普通例子–斐波那契数列开始。下面是使用递归的常规实现。
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
与大多数其他编程语言一样,Python
也需要为每层递归函数建立一个 “栈”,并计算每个栈上的值。
不过,使用cache
装饰器将大大提高代码性能。而且,这样做并不困难。我们只需从 functools
模块中导入,然后将装饰器添加到函数中即可。
from functools import cache
@cache
def fibonacci_cached(n):
if n < 2:
return n
return fibonacci_cached(n-1) + fibonacci_cached(n-2)
3. 性能对比
以下是两种实现的运行结果和性能对比:
结果显示,启用缓存版本的性能比不启用缓存的版本大约快 120 倍!顺便说一下,上述代码中,我给 %timeit
神奇命令设置了-r 1 -n 1
参数,以确保函数只执行一次。否则,缓存的 Fibonacci
函数会非常快。例如,如果我们运行函数 10000 次,除了第一次,其余 9999 次的结果都将直接从缓存中加载。因此,上述参数可确保测试只执行一次。
4. 递归场景原因
首先让我们来看看这个斐波那契递归函数的堆栈调用。为了确保能在图像中演示,我们必须简化场景。下图中显示的是 fibonacci(4)
的堆栈。
当我们计算函数 fibonacci(4)
时,递归函数将调用下一级的新参数,直到调用到初始值fibonacci(1)==1
和fibonacci(0)==0
为止。在上图中,所有步骤都需要计算。例如,尽管在计算法f(4)
时, f(0)
、f(1)
和 f(2)
已经计算了多次,但它们都是分别重复计算的。
5. 缓存场景原因
接着,让我们看看使用缓存装饰器后的情况:
这一次,绿色的步骤不再需要重复计算。只要函数 f(x)
计算过一次,就会被缓存起来。然后,当 f(x)
再次出现时,结果将直接从内存中加载,因为它已被缓存。因此,在上图中,灰色步骤根本不需要计算。
所以,真正的堆栈如下图所示。部分f(x)
函数将直接从缓存中加载。
从上图中我们不难理解,只有左侧边上的步骤会被实际计算,而且每个 f(x)
只计算一次。这也是启用缓存后性能远高于普通递归函数的原因。此外,对于斐波那契函数这个特殊示例,我们可以得出,使用的参数数字越大,缓存带来的性能提升就越大。例如,fibonacci_cached(30)
的性能将是 fibonacci(30)
的 120
倍。
6. 使用场景
当然,并不是在所有地方都建议使用缓存。在我们开始在每个Python
函数上添加 @cache
之前,需要考虑以下事项。
-
Python版本
请注意,@cache
装饰器是在Python 3.9中引入的。如果你不能使用 3.9+ 版本,请考虑使用@lru_cache
,它是一个更全面的缓存功能。我将在后续文章中介绍。 -
不可在非确定函数中使用缓存
当函数中存在任何非确定性内容时,我们就不应该使用缓存。例如我们使用函数datetime.now()
来获取当前的时间戳。获取当前时间戳的操作是非确定的。就是每次执行返回的值是不同的,若使用缓存,可能会导致两次不同时间调用会返回同样的时间戳,这是不能接受的。 -
若函数有副作用,则不建议使用缓存
这里说的 "副作用 "是指返回值之外的操作,例如向文件写入文本或更新数据库表。如果我们对这些函数使用缓存,"副作用 "就不会在第二次调用时发生。换句话说,只有当我们第一次调用函数时,它才会起作用。
7. 总结
总之,我们在本文中介绍了Python
内置functools
模块中的@cache
装饰器。它可以用来提高一些典型递归函数的性能,也可以被认为是在 Python 应用程序中实现缓存功能的最简单方法。
您学废了嘛?
扫码进群,交个朋友!