news 2026/8/21 13:59:54

如何使用 Python 内置装饰来显著提高性能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何使用 Python 内置装饰来显著提高性能

原文:towardsdatascience.com/how-to-use-python-built-in-decoration-to-improve-performance-significantly-4eb298f248e1

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/58d7a342065e9269df9c5c5f7ec18f16.png

图片由作者在 Canva 中创建

当谈到提高 Python 执行性能时,尤其是在数据处理方面,有太多第三方库可以帮助我们。如果我们考虑它们的机制,大多数都依赖于优化数据结构或内存利用来达到性能提升。

例如,Dask 利用并行计算和内存优化,Pandas 依赖于数据集的向量化,Modin 也优化了多核 CPU 和内存的利用。

在这篇文章中,我不会介绍任何库。事实上,有一个原生的 Python 装饰可以用来显著提高性能。我们不需要安装任何东西,因为它内置在 Python 中。当然,它不会在所有场景下都使用。所以,在最后一节,我还会讨论我们不应该使用它的情况。

1. 缓存装饰的一个用例

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5093c86bddadba9b6d0a524b879939f3.png

图片由作者在 Canva 中创建

让我们从大家都很熟悉的简单例子开始,那就是 斐波那契数列。下面是一个使用递归的正常实现。

deffibonacci(n):ifn<2:returnnreturnfibonacci(n-1)+fibonacci(n-2)

就像大多数其他编程语言一样,Python 也需要为递归函数构建一个“栈”并在每个栈上计算值。

然而,“缓存”装饰将显著提高性能。而且,这样做并不困难。我们只需要从functools模块导入它,然后将装饰添加到函数中。

fromfunctoolsimportcache@cachedeffibonacci_cached(n):ifn<2:returnnreturnfibonacci_cached(n-1)+fibonacci_cached(n-2)

这里是运行结果和性能比较。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/8c7d77b6675b39746ab2dc0a3bd79740.png

这表明启用缓存的版本的性能大约比未启用缓存的版本快120 倍

BTW,我已经给%timeit魔法命令设置了-r 1 -n 1参数,以确保函数只会执行一次。否则,缓存的斐波那契函数会非常快。例如,如果我们运行函数 10000 次,除了第一次,其他 9999 次的结果将直接从缓存中加载。因此,这些参数确保测试只执行一次。

2. 缓存装饰如何提高性能?

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/d0cf70a2e204509875190c0014cfe7c5.png

图片由作者在 Canva 中创建

让我们看看这个斐波那契递归函数的调用栈。为了确保它可以在图中演示,我们必须简化场景。图显示了fibonacci(4)的调用栈。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/19c0a629ed64a05f41b768e852356137.png

无缓存的斐波那契递归函数

当我们调用fibonacci(4)函数时,递归函数将在下一个较低的级别以新的参数调用自身,直到达到基本案例fibonacci(1)==1fibonacci(0)==0

在上面的图中,所有步骤都需要计算。例如,即使f(0)f(1)f(2)出现多次,它们都是单独计算的。

现在,让我们看看启用缓存的情况。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/479ca326354d2d70c9633639c21ecc64.png

带缓存的斐波那契递归函数 – 所有步骤

这次,绿色标记的步骤不再需要计算。只要函数f(x)被计算过一次,它就会被缓存。然后,当f(x)再次发生时,结果将直接从内存中加载,因为它已经被缓存。

因此,在上面的图中,灰色步骤甚至不需要计算。所以,实际的调用栈将类似于以下内容。一些f(x)函数将直接从缓存中加载。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5589b8c676fe6024f72098a4a734f2cc.png

带缓存的斐波那契递归函数 – 实际步骤

如果我们回到上面代码中的例子,fibonacci_cached(20)将看起来如下。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/82066234985f05ed128cc670c2aa0726.png

带缓存的斐波那契递归函数 – f(20),部分演示

从图中我们可以轻松理解,只有左边的步骤会被实际计算,每个f(x)只会被计算一次。

这就是为什么启用缓存后的性能比普通递归函数高得多的原因。此外,对于这个特定的例子,斐波那契函数,我们可以推导出,使用的参数数值越大,缓存带来的性能提升就越大。例如,fibonacci_cached(30)的性能将比fibonacci(30)高出 120 多倍。

3. 来自现实世界的实际例子

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9fa9643e6da224eed570d2177e1ecb84.png

作者在 Canva 中创建的图像

假设我们正在开发一个基于 Python 的数据仪表板,该仪表板拥有许多用户。仪表板显示美国 5 个城市的天气数据,并允许用户筛选和汇总特定城市的温度数据。

以下代码将生成一些模拟数据作为数据集。

importpandasaspdimportnumpyasnpfromdatetimeimportdatetime,timedeltafromfunctoolsimportcache# Create sample data: hourly temperature recordings over 10 days for 5 citiesnp.random.seed(0)date_range=pd.date_range(start="2024-04-01",end="2024-04-10",freq='H')data=pd.DataFrame({'timestamp':np.tile(date_range,5),'city':np.repeat(['New York','Los Angeles','Chicago','Houston','Phoenix'],len(date_range)),'temperature':np.random.normal(loc=15,scale=10,size=(len(date_range)*5,))})

这里是样本数据的输出。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b7ecee7026d123bf85515b216d716070.png

然后,让我们编写一个计算城市平均温度的方法。当然,我们需要两个版本,一个是正常的,另一个带有“缓存”装饰。

defcompute_avg_daily_temp(date,city):day_data=data[(data['timestamp'].dt.date==pd.to_datetime(date).date())&amp;(data['city']==city)]returnday_data['temperature'].mean()@cachedefcompute_avg_daily_temp_cached(date,city):day_data=data[(data['timestamp'].dt.date==pd.to_datetime(date).date())&amp;(data['city']==city)]returnday_data['temperature'].mean()

我们可以调用这些函数来检查它们是否正常工作。

compute_avg_daily_temp('2024-04-09','New York')compute_avg_daily_temp_cached('2024-04-09','New York')

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/cbf3ca113072456aac1c214e350dd7c3.png

现在,让我们看看性能。请注意,如果您也测试了函数的输出,请不要使用相同的日期。在上面的测试中,我使用了日期2024–04–09,因此我将在以下性能测试中使用不同的日期2024–04–10。这是为了避免结果被缓存并导致测试结果不准确。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/936db753702e95f446ad0c48e637c345.png

在单元格 [9] 和 [10] 中,正常版本和缓存版本的性能相同。这些是这些函数分别执行的第一轮运行。

然后,对于它们的第二次运行,正常函数在单元格 [11] 中的性能几乎没有改进。请注意,8.82ms 和 5.86ms 并不代表后者性能更好,因为我们只运行了一次。操作系统的波动可能会非常常见地造成这种差异。

然而,当我们查看单元格 [12] 时,缓存函数的性能大约是1,733 倍更快。这是因为实际上并没有进行计算。结果是从缓存中加载的。

为什么这个例子是实用的?

您可能会问为什么我们需要为这个例子实现缓存机制。当然,如果我们只想计算一个城市的平均温度,我们根本不需要缓存。然而,考虑到我们正在开发一个数据驱动的仪表板应用程序。因此,缓存功能将推动以下实践。

如果我们有多位用户使用这个仪表板应用程序,他们可能会查询相同的数据库集,并要求相同的聚合或计算指标。因此,缓存功能将帮助第 2 个和所有其他用户在极短的时间内获得结果。

在这个例子中,我们每天使用的数据对于每天都是一致的。也就是说,一旦计算了某一天的平均温度,它就不会改变。

在大多数其他编程语言中,实现这样的缓存机制可能要困难得多。然而,我们已经看到在 Python 中这可以更容易实现。

4. 其他考虑事项和我们应该何时不使用缓存

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/bdd50f61abcb29ec72f297a100c76747.png

由作者在 Canva 中创建的图像

当然,并不建议在所有地方都使用缓存。在您开始在每个 Python 函数上添加@cache之前,以下事项需要考虑。

您的 Python 版本

请注意,@cache装饰器是在 Python 3.9 中引入的。如果您不能使用 3.9 及以上版本,请考虑使用@lru_cache,这是一个更全面的缓存功能。我将在下一篇文章中介绍这个装饰器。

不要在非确定性函数中使用缓存

当函数中存在任何非确定性内容时,我们绝不应该使用缓存。让我们用上面相同的例子来计算平均温度。假设我们将要求改为“获取今天的平均温度”。我们可能需要在这个函数中添加datetime.now()来获取当前的时间戳。获取当前时间戳的行为是非确定性的。

如果你不明白我的意思,请看这个例子。

fromdatetimeimportdatetimefromfunctoolsimportcache@cachedefget_current_time_cached():returndatetime.now()

上述函数是最简单的非确定性函数。在我们运行函数之前,让我们同时运行datetime.now()函数和缓存的函数。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f89c3c0c1eab44a0a12d8ff1e4b5f536.png

我们可以看到,即使我非常快地运行了两个单元格,如果我们使用datetime.now(),时间戳仍然不同。这是有道理的,因为时间在流逝。然而,无论我们调用缓存的函数多少次,时间戳都不会再改变。我们在我们的应用程序中引入了一个严重的错误!

同样的原则也适用于基于随机数的函数。如果我们的随机函数每次都生成相同的结果,那么它就不再是随机的了。

如果函数有“副作用”,不要使用缓存

“副作用”的意思是指除了返回值之外的动作,例如将一些文本写入文件或更新数据库表。如果我们对这些函数使用缓存,那么“副作用”将永远不会在第二次调用时发生。换句话说,它只会在我们第一次调用函数时起作用。

如果有内存大小方面的担忧,不要使用缓存

Python 将这些缓存结果存储在哪里?当然是在计算内存中。将一个城市的平均温度存储起来,或者甚至存储 100 个城市的平均温度,这当然是可以的。然而,如果我们想缓存过去一年中所有主要城市每小时滚动平均温度,那将是一个糟糕的主意。

然而,只是强调一下,如果我们想缓存许多小的结果,但又担心结果集可能太多,那么再次使用lru_cache将是一个很好的用例。请关注我的个人资料。我稍后会有一篇关于这个装饰器的文章。

如果数据是安全敏感的,不要使用缓存

记住,缓存装饰器不会加密它在内存中缓存的任何结果。这意味着当前操作系统中可能存在其他进程可能会获取到内存中缓存的这些信息。

因此,出于安全考虑,请勿缓存任何敏感数据。

总结

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b41c2ea0f99255fcb30369033956168f.png

图片由作者在 Canva 中创建

总结一下,我们在本文中介绍了 Python 内置的functools模块中的@cache装饰器。它可以用来提高一些典型递归函数的性能,也可以被认为是实现 Python 应用程序中缓存功能的最简单方法。

当然,这并不是一个完美且适用于所有情况的解决方案。我们应该意识到某些场景是我们绝对不应该使用的,例如与非确定性函数一起使用。同时,我们还需要注意内存的使用,因为缓存的結果将会保留在内存中。如果你对更灵活的缓存功能感兴趣,但觉得使用起来稍微有些困难,请关注我的个人资料,它即将推出。

本文中的所有图片均由作者创作

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 17:23:45

Jupyter Lab安装扩展插件增强功能体验

Jupyter Lab 安装扩展插件增强功能体验 在现代 AI 开发中&#xff0c;一个稳定、高效且可扩展的交互式编程环境几乎是每个数据科学家和算法工程师的刚需。尽管 Jupyter Notebook 曾经风靡一时&#xff0c;但面对日益复杂的项目结构、多任务并行需求以及团队协作挑战&#xff0c…

作者头像 李华
网站建设 2026/8/19 17:26:44

Jupyter Notebook单元格执行顺序注意事项

Jupyter Notebook 单元格执行顺序的工程实践警示 在深度学习实验中&#xff0c;你是否曾遇到这样的情况&#xff1a;同一份代码昨天运行正常&#xff0c;今天却报出 NameError&#xff1f;或者模型训练结果莫名其妙地“漂移”了&#xff1f;这类问题往往并非算法本身有误&…

作者头像 李华
网站建设 2026/8/19 18:13:34

Git worktree创建多个PyTorch工作树并行开发

Git Worktree 与 PyTorch-CUDA 并行开发实践 在深度学习项目中&#xff0c;开发者常常面临这样的困境&#xff1a;一边是正在调试的模型结构改动&#xff0c;另一边是紧急修复线上推理服务的 bug&#xff1b;一个分支在跑长周期训练任务&#xff0c;另一个分支又要尝试新的数据…

作者头像 李华
网站建设 2026/8/19 18:06:44

x64dbg下载配合虚拟机调试:完整示例说明

从零开始构建安全逆向环境&#xff1a;x64dbg 虚拟机实战指南 你有没有过这样的经历&#xff1f;刚下载了一个CTF的CrackMe程序&#xff0c;兴冲冲地双击运行&#xff0c;结果系统弹出一堆警告&#xff0c;杀软瞬间报警——这还怎么调试&#xff1f; 更吓人的是&#xff0c;…

作者头像 李华
网站建设 2026/8/18 18:49:17

MIPS ALU设计:定点运算核心要点解析

MIPS ALU设计&#xff1a;从加法器到控制信号的硬核拆解你有没有想过&#xff0c;当你写下一行简单的 C 代码a b c;&#xff0c;背后到底发生了什么&#xff1f;在 CPU 内部&#xff0c;并不是“直接相加”这么简单。这条语句最终会被编译成一条如ADD $t0, $t1, $t2的 MIPS 指…

作者头像 李华
网站建设 2026/8/9 5:08:17

面向工业自动化的Vitis平台搭建详解

从零搭建工业自动化中的 Vitis 开发环境&#xff1a;实战全解析当工业控制遇上自适应计算在智能制造的浪潮下&#xff0c;传统的PLC和单片机方案已难以满足现代工业系统对实时性、灵活性与智能化的复合需求。越来越多的高端设备开始采用“ARM FPGA”异构架构——比如 Xilinx 的…

作者头像 李华