news 2026/4/28 10:40:46

如何使用 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/4/28 10:40:44

2024年AI原生应用趋势:事件驱动架构深度解析

2024年AI原生应用趋势&#xff1a;事件驱动架构深度解析 关键词&#xff1a;事件驱动架构、AI原生应用、事件流、实时处理、解耦设计、微服务、持续学习 摘要&#xff1a;2024年&#xff0c;AI原生应用&#xff08;AI-Native Applications&#xff09;正从“能用”向“好用”快…

作者头像 李华
网站建设 2026/4/25 14:49:37

大模型推理延迟优化:GPU加速+Token流式输出

大模型推理延迟优化&#xff1a;GPU加速与流式输出的协同实践 在今天的AI应用中&#xff0c;用户已经不再满足于“能不能回答”&#xff0c;而是更关心“多久能答出来”。当你向一个智能助手提问时&#xff0c;哪怕只是多等一两秒&#xff0c;那种轻微的卡顿感也会悄然削弱信任…

作者头像 李华
网站建设 2026/4/25 22:24:43

使用Markdown表格整理PyTorch函数对照清单

使用 Markdown 表格整理 PyTorch 函数对照清单 在深度学习项目中&#xff0c;一个常见的挑战是团队成员之间对函数用法的理解不一致&#xff0c;尤其是在跨版本迁移或协作开发时。PyTorch 虽然以易用著称&#xff0c;但其 API 在不同版本间仍存在细微差异&#xff0c;加上 CUDA…

作者头像 李华
网站建设 2026/4/25 11:49:05

PyTorch反向传播机制深入理解与调试技巧

PyTorch反向传播机制深入理解与调试技巧 在现代深度学习实践中&#xff0c;模型训练的稳定性往往取决于开发者对底层机制的理解程度。即便使用了如PyTorch这样“开箱即用”的框架&#xff0c;一旦遇到梯度爆炸、NaN损失或参数不更新等问题&#xff0c;若仅停留在调用 .backward…

作者头像 李华
网站建设 2026/4/23 17:20:26

PyTorch镜像中实现梯度裁剪(Gradient Clipping)防止爆炸

PyTorch镜像中实现梯度裁剪防止梯度爆炸 在深度学习的实践中&#xff0c;你是否曾遇到训练进行到一半&#xff0c;损失突然变成 NaN&#xff0c;模型彻底“死亡”&#xff1f;尤其是在训练RNN、Transformer这类深层或序列模型时&#xff0c;这种现象尤为常见。问题的根源往往不…

作者头像 李华
网站建设 2026/4/23 14:29:31

D触发器电路图电平触发与边沿触发区别:一文说清

D触发器电路图电平触发与边沿触发区别&#xff1a;一文说清 在数字电路的世界里&#xff0c; D触发器电路图 几乎是每个工程师都绕不开的核心元件。无论你是设计一个简单的计数器&#xff0c;还是构建复杂的CPU流水线&#xff0c;D触发器都是实现数据同步、状态保持和时序控制…

作者头像 李华