加速 Python 程序的 10 个技巧

B站影视 2025-01-14 17:05 3

摘要:总的来说,人们使用 Python 是因为它方便且对程序员友好,而不是因为它快速。大量的第三方库和对 Python 的广泛行业支持在很大程度上弥补了它不具备 Java 或 C 的原始性能。开发速度优先于执行速度。

有很多方法可以提高 Python 应用程序的性能。以下是加快 Python 速度的 10 个硬核编码技巧。

总的来说,人们使用 Python 是因为它方便且对程序员友好,而不是因为它快速。大量的第三方库和对 Python 的广泛行业支持在很大程度上弥补了它不具备 Java 或 C 的原始性能。开发速度优先于执行速度。

但在许多情况下,它不一定是一个非此即彼的命题。经过适当优化后,Python 应用程序可以以惊人的速度运行,虽然速度可能不如 Java 或 C 语言快,但对于 Web 应用程序、数据分析、管理和自动化工具以及大多数其他用途来说已经足够快了。通过正确的优化,您甚至可能不会注意到应用程序性能和开发人员工作效率之间的权衡。

优化 Python 性能并不归结为任何一个因素。相反,它是关于应用所有可用的最佳实践并选择最适合手头场景的最佳实践。(Dropbox 的人们有一个最令人瞠目结舌的例子,展示了 Python 优化的力量。

在本文中,我将讨论 10 种常见的 Python 优化。有些是临时措施,只需要将一个项目换成另一个项目(例如更改 Python 解释器);其他 API 的回报更高,但也需要更详细的工作。

正如那句老话所说,你不能错过你不衡量的东西。同样,如果不找出缓慢的原因,就无法找出任何给定 Python 应用程序运行不佳的原因。

通过 Python 的内置 cProfile 模块从简单的分析开始,如果您需要更高的精度或更深入的洞察,请转向更强大的分析器。通常,通过对应用程序进行基本功能级检查所收集的见解提供了足够的视角。(您可以通过 profilehooks模块提取单个函数的配置文件数据。

为什么应用程序的特定部分如此缓慢以及如何修复它,可能需要更多的挖掘。关键是缩小关注范围,使用硬数字建立基线,并尽可能在各种使用和部署场景中进行测试。不要过早优化。猜测让你无处可去。

来自 Dropbox 的示例(上面链接)显示了分析的有用性。“正是测量告诉我们 HTML 转义一开始很慢,”开发人员写道,“如果不测量性能,我们永远不会猜到字符串插值会如此缓慢。

当您可以执行一次并保存结果时,永远不要执行一千次工作。如果您有一个经常调用的函数,该函数返回可预测的结果,Python 会为您提供将结果缓存到内存中的选项。返回相同结果的后续调用几乎会立即返回。

各种示例说明了如何执行此操作;我最喜欢的记忆几乎是尽可能少的。但是 Python 内置了此功能。Python 的原生库之一 functools 具有 @functools.lru_cache装饰器,它缓存对函数的 n 次最新调用。当您缓存的值发生变化但在特定时间窗口内相对静态时,这很方便。一天中最近使用的项目列表就是一个很好的例子。

请注意,如果您确定对函数的调用种类将保持在合理的范围内(例如,100 个不同的缓存结果),则可以使用性能更高的 @functools.cache。

如果您正在进行基于矩阵或基于数组的数学运算,并且不希望 Python 解释器妨碍您,请使用 NumPy。通过利用 C 库来完成繁重的工作,NumPy 提供了比原生 Python 更快的数组处理。它还比 Python 的内置数据结构更有效地存储数值数据。

NumPy 的另一个好处是可以更有效地将内存用于大型对象,例如包含数百万个项目的列表。平均而言,像 NumPy 中的大型对象占用的内存大约是用传统 Python 表示时所需内存的四分之一。请注意,从作业的正确数据结构开始会有所帮助,这本身就是一种优化。

重写 Python 算法以使用 NumPy 需要一些工作,因为数组对象需要使用 NumPy 的语法声明。此外,最大的加速来自使用 NumPy 特定的 “广播” 技术,其中函数或行为应用于数组。花点时间深入研究 NumPy 的文档,了解有哪些功能可用以及如何很好地使用它们。

此外,虽然 NumPy 适合加速基于矩阵或数组的数学运算,但它并不能为在 NumPy 数组或矩阵之外执行的数学运算提供有用的加速。涉及传统 Python 对象的数学不会加速。

另一个用于加速数学运算的强大库是 Numba。编写一些用于数值操作的 Python 代码,并使用 Numba 的 JIT(即时)编译器包装它,生成的代码将以机器原生的速度运行。Numba 不仅提供 GPU 驱动的加速(CUDA 和 ROC),而且还具有特殊的“nopython”模式,该模式试图通过尽可能不依赖 Python 解释器来最大限度地提高性能。

Numba 还与 NumPy 携手合作,因此您可以两全其美 — NumPy 可以解决所有操作,而 Numba 可以解决所有其他操作。

NumPy 使用用 C 编写的库是一个很好的模拟策略。如果现有的 C 库可以满足您的需要,Python 及其生态系统提供了多种选项来连接到该库并利用其速度。

最常见的方法是 Python 的 ctypes 库。由于 ctypes 与其他 Python 应用程序(和运行时)广泛兼容,因此它是最好的起点,但它远非唯一的游戏。CFFI 项目为 C. Cython(见下文)提供了更优雅的接口,也可用于编写您自己的 C 库或包装外部、现有库,但代价是必须学习 Cython 的标记。

这里有一个警告:通过最大限度地减少跨越 C 和 Python 之间边界的往返次数,您将获得最佳结果。每次在它们之间传递数据时,都会对性能造成影响。如果可以选择在紧密循环中调用 C 库,也可以选择将整个数据结构传递给 C 库并在其中执行循环内处理,请选择第二个选项。您将减少域之间的往返次数。

如果您想要速度,请使用 C 语言,而不是 Python。但对于 Pythonistas 来说,编写 C 代码会带来许多干扰 — 学习 C 的语法、整理 C 工具链(现在我的头文件怎么了?)等等。

Cython 允许 Python 用户方便地访问 C 的速度。现有的 Python 代码可以增量转换为 C 语言 — 首先使用 Cython 将所述代码编译为 C,然后添加类型注释以提高速度。

Cython 不是一根神奇的魔杖。将代码按原样转换为 Cython,如果没有类型公告,则运行速度通常不会超过 15% 到 50%。这是因为该级别的大多数优化都侧重于减少 Python 解释器的开销。当您的变量可以注释为 C 类型时,最大的收益来自于此,例如,机器级 64 位整数,而不是 Python 的 int 类型。由此产生的加速可以提高几个数量级。

CPU 密集型代码从 Cython 中受益最大。如果你已经分析了(你已经分析了,不是吗?)并发现代码的某些部分使用了绝大多数 CPU 时间,那么这些是 Cython 转换的极好候选者。受 I/O 限制的代码(如长时间运行的网络操作)从 Cython 获得的好处很小或根本没有。

与使用 C 库一样,另一个重要的性能增强技巧是将到 Cython 的往返次数保持在最低限度。不要编写重复调用“Cythonized”函数的循环;在 Cython 中实现循环并一次性传递数据。

传统的 Python 应用程序(在 CPython 中实现的应用程序)一次只执行一个线程,以避免在使用多个线程时出现的状态问题。这就是臭名昭著的全局解释器锁 (GIL)。它的存在有充分的理由,但这并没有使它变得不那么糟糕。

CPython 应用程序可以是多线程的,但由于 GIL 的原因,CPython 并不真正允许这些线程在多个内核上并行运行。随着时间的推移,GIL 的效率大大提高,并且正在努力完全消除它,但目前核心问题仍然存在。

一种常见的解决方法是 multiprocessing 模块,该模块在单独的内核上运行 Python 解释器的多个实例。状态可以通过共享内存或服务器进程共享,并且数据可以通过队列或管道在进程实例之间传递。

您仍然必须在进程之间手动管理状态。此外,启动多个 Python 实例并在它们之间传递对象涉及的开销不小。但对于受益于跨内核并行性的长时间运行的进程,multiprocessing 库很有用。

顺便说一句,使用 C 库(例如 NumPy 或 Cython)的 Python 模块和包能够完全避免 GIL。这是推荐它们以提升速度的另一个原因。

只需键入 include foobar 并利用无数其他程序员的工作是多么方便啊!但您需要注意,第三方库可能会改变应用程序的性能,但并不总是变得更好。

有时,这以明显的方式表现出来,例如当来自特定库的模块构成瓶颈时。(同样,分析会有所帮助。有时它不那么明显。例如,考虑 Pyglet,这是一个用于创建窗口图形应用程序的便捷库。Pyglet 会自动启用调试模式,这会极大地影响性能,直到它被显式禁用。除非您阅读库的文档,否则您可能永远不会意识到这一点,因此当您开始使用新库时,请仔细阅读并了解情况。

Python 可以跨平台运行,但这并不意味着每个操作系统(Windows、Linux、macOS)的特性在 Python 下完全抽象出来。大多数时候,了解平台细节(如路径命名约定)是值得的,其中有帮助程序函数。例如,pathlib 模块抽象出特定于平台的 path 约定。控制台处理在 Windows 和其他操作系统之间也有很大差异;因此,像 rich 这样的抽象库很受欢迎。

在某些平台上,某些功能根本不受支持,这可能会影响您编写 Python 的方式。例如,Windows 没有进程分叉的概念,因此一些多处理功能在那里的工作方式不同。

最后,Python 本身在平台上的安装和运行方式也很重要。例如,在 Linux 上, pip 通常与 Python 本身分开安装;在 Windows 上,它会随 Python 自动安装。

CPython 是最常用的 Python 实现,它优先考虑兼容性而不是原始速度。对于希望将速度放在首位的程序员,可以使用 PyPy,这是一种配备 JIT 编译器的 Python 实现,可加速代码执行。

由于 PyPy 被设计为 CPython 的直接替代品,因此它是快速提高性能的最简单方法之一。许多常见的 Python 应用程序将完全按原样在 PyPy 上运行。通常,应用程序对 “vanilla” Python 的依赖程度越高,它就越有可能在不修改的情况下在 PyPy 上运行。

但是,充分利用 PyPy 可能需要测试和研究。您会发现,长时间运行的应用程序从 PyPy 中获得的性能提升最大,因为编译器会分析一段时间内的执行情况,以确定如何加快速度。对于仅运行和退出的短脚本,最好使用 CPython,因为性能提升不足以克服 JIT 的开销。

请注意,PyPy 对 Python 的支持往往滞后于该语言的最新版本。当 Python 3.12 是最新的时,PyPy 仅支持最高版本 3.10。此外,使用 ctypes 的 Python 应用程序可能并不总是按预期运行。如果您正在编写可能同时在 PyPy 和 CPython 上运行的内容,则单独处理每个解释器的用例可能是有意义的。

来源:AI中国

相关推荐