介绍
Python或许是当今最流行的编程语言之一,但它的效率绝对不是最高的。尤其是在机器学习领域,用户为了Python的易用性而牺牲了效率。.
但这并不意味着你不能通过其他方式加快速度。Cython 提供了一种简单的方法,可以在不牺牲 Python 本身易于实现的功能的前提下,减少 Python 脚本的计算时间。.
本教程将介绍如何使用 Cython 来加速 Python 脚本。我们将解决一个简单但计算量巨大的任务:创建一个 for 循环,遍历一个包含 10 亿个数字的 Python 列表并求和。由于在资源受限的设备上运行代码时时间至关重要,我们将探讨如何在 Raspberry Pi (RPi) 上使用 Cython 实现 Python 代码。Cython 可以显著提升计算速度,就像是使用惰性求值程序 (Lazy) 和使用 Yozo 程序 (yozo) 之间的巨大差异。.
使用 Cython 优化 Python 脚本的前提条件
- Python基础知识:熟悉Python语法、函数、数据类型和模块。.
- 理解 C/C++ 基本概念:熟悉 C 或 C++ 的基本概念,例如指针、数据类型和控制结构。.
- Python 开发环境:使用 pip 等包管理器安装 Python(最好是 Python 3.x)。.
- 安装 Cython:使用 pip install cython 命令安装 Cython。.
- 终端/命令行熟悉度:具备在终端或命令行中导航和执行命令的基本能力。.
这些先决条件将帮助你做好准备,开始使用 Cython 优化 Python 代码。.
Python 和 CPython
很多人并不知道,像 Python 这样的语言实际上是用其他语言实现的。例如,Python 的 C 语言实现被称为 CPython。请注意,这与 Cython 不同。想要了解更多关于不同 Python 实现的信息,您可以阅读这篇文章。.
Python 的默认且最流行的实现是 C-Python。使用它有一个重要的优势。C 是一种编译型语言,它的代码会被转换成机器代码,直接由中央处理器 (CPU) 执行。现在你可能会问,如果 C 是编译型语言,那么 Python 也是吗?
C 语言实现的 Python (Cpython) 100% 既不是编译型的,也不是解释型的。实际上,编译和解释都是在执行 Python 脚本的过程中发生的。为了更清楚地说明这一点,让我们看一下执行 Python 脚本的步骤:
- 使用 CPython 编译源代码以生成字节码
- CPython解释器对字节码的解释
- 在 CPython 虚拟机中运行 CPython 解释器的输出
编译过程是指 CPython 编译源代码(.py 文件)并生成 CPython 字节码(.pyc 文件)。然后,CPython 解释器会解释该字节码,并在 CPython 虚拟机中执行其输出。如上所示,执行 Python 脚本的过程涉及编译和解释两个步骤。.
CPython 编译器只生成一次字节码,但每次执行代码时都会调用解释器。解释字节码通常需要很长时间。如果使用解释器会降低执行速度,那为什么还要使用它呢?主要原因是解释器使 Python 能够在不同的操作系统上运行。由于字节码是在运行于 CPU 上的 CPython 虚拟机中独立于机器执行的,因此无需任何修改即可在不同的机器上运行。.
如果不使用解释器,CPython 编译器将生成直接在 CPU 上运行的机器代码。由于不同平台的指令集不同,因此该代码无法在不同的平台上运行。.
简而言之,使用编译器可以加快处理速度,而解释器则使代码能够跨平台运行。因此,Python 比 C 慢的原因之一就是使用了解释器。请记住,编译器只运行一次,而解释器每次执行代码时都会运行。.
Python 比 C 慢得多,但许多程序员仍然更喜欢它,因为它更容易使用。Python 为程序员隐藏了许多细节,这可以避免令人沮丧的调试过程。例如,由于 Python 是一种动态类型语言,因此无需在代码中指定每个变量的类型——Python 会自动推断。相比之下,在静态类型语言(例如 C、C++ 或 Java)中,您必须指定变量的类型,如下所示。.
int x = 10
string s = "Hello"与以下 Python 实现进行比较:
动态类型使编码更容易,但它增加了机器查找正确数据类型的负担,从而减慢了执行速度。.
x = 10
s = "Hello"一般来说,像 Python 这样的“高级”语言对开发者来说更容易理解。但是,代码执行时需要转换成低级指令。这种转换需要更多时间,而这是为了保证易用性而做出的牺牲。.
如果时间紧迫,你应该使用底层命令。也就是说,与其用 Python(前端语言)编写代码,不如使用 CPython,它底层也是用 Python 编写的,但底层是用 C 语言实现的。不过,这样做的话,你会感觉自己像是在用 C 语言编程,而不是 Python。.
CPython 要复杂得多。在 CPython 中,所有代码都是用 C 语言实现的。编写代码时,无法避免 C 语言的复杂性。这就是为什么许多开发者选择使用 Cython 的原因。那么,Cython 与 CPython 究竟有何不同呢?
Cython 有何不同?
如上所述,Cython 是一种兼具速度和易用性的语言。您仍然可以用 Python 编写常规代码,但为了提高执行速度,Cython 允许您用 C 代码替换部分 Python 代码。这样,您最终可以将两种语言结合在一个文件中。请注意,您可以想象 Python 中的所有代码在 Cython 中都有效,但存在一些限制。.
普通的 Python 文件扩展名为 .py,而 Cython 文件扩展名为 .pyx。同样的 Python 代码可以写在 .pyx 文件中,但这些文件也允许你使用 Cython 代码。需要注意的是,将 Python 代码放在 .pyx 文件中可能比直接运行 Python 代码更快,但不如声明变量类型来得快。因此,本教程的重点不仅在于如何在 .pyx 文件中编写 Python 代码,还在于如何进行修改以提升代码运行速度。这会增加一些编程的复杂性,但可以节省大量时间。如果你有一些 C 语言编程经验,这将更容易理解。.
简单的 Python 代码
要将 Python 代码转换为 Cython,首先需要创建一个扩展名为 .py 的文件。 .pyx 创建而非扩展 .py. 在这个文件中,你可以开始编写常规的 Python 代码(请注意,Cython 对代码有一些限制,这些限制在 Cython 文档中有说明)。.
在继续操作之前,请确保已安装 Cython。您可以使用以下命令完成此操作。.
pip install cython
要生成 .pyd/.so 文件,我们首先需要构建 Cython 文件。.pyd/.so 文件代表我们稍后将要导入的模块。构建 Cython 文件需要使用 setup.py 文件。创建此文件并将以下代码放入其中。我们将使用 distutils.core.setup() 函数调用 Cython.Build.cythonize() 函数,该函数将对 .pyx 文件进行 Cyanogenize 处理。此函数接受要进行 Cyanogenize 处理的文件的路径。这里,我假设 setup.py 文件与 test_cython.pyx 文件位于同一目录下。.
import distutils.core
import Cython.Build
distutils.core.setup(
ext_modules = Cython.Build.cythonize("test_cython.pyx"))要构建 Cython 文件,请在命令行中输入以下命令。命令行当前目录必须与 setup.py 文件所在的目录相同。.
python setup.py build_ext --inplace
此命令执行完毕后,会在 .pyx 文件旁边生成两个文件。第一个文件的扩展名为 .c,另一个文件的扩展名为 .pyd(或类似扩展名,具体取决于操作系统)。要使用生成的文件,只需导入 test_cython 模块,即可直接显示“Hello Cython”消息,如下所示。.
我们现在已经成功地对 Python 代码进行了 Cyton 化。下一节将介绍如何对包含循环的 .pyx 文件进行 Cyton 化。.
胞质分裂“for”环”
现在我们来优化之前的任务:一个遍历一百万个数字并求和的 for 循环。首先,我们来考察一下循环本身的效率。时间模块会用来估算循环的执行时间。.
import time
t1 = time.time()
for k in range(1000000):
pass
t2 = time.time()
t = t2-t1
print("%.20f" % t)在一个 .pyx 文件中,执行 3 次的平均时间为 0.0281 秒。该代码运行在一台配备 Core i7-6500U @ 2.5 GHz 处理器和 16 GB DDR3 内存的机器上。.
相比之下,典型的 Python 文件执行时间平均为 0.0411 秒。这意味着,即使不修改 for 循环使其以 C 语言的速度运行,Cython 在迭代次数上也仅比 Python 快 1.46 倍。.
现在我们来添加加法运算。我们将使用 range() 函数来实现这一点。.
import time
t1 = time.time()
total = 0
for k in range(1000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)请注意,两个脚本返回的值相同,均为 499999500000。在 Python 中,该脚本平均运行时间为 0.1183 秒(三次测试结果)。在 Cython 中,速度提升了 1.35 倍,平均运行时间为 0.0875 秒。.
现在让我们来看另一个例子,循环从 0 开始,遍历 10 亿个数字。.
import time
t1 = time.time()
total = 0
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.20f" % t)Cython脚本大约耗时85秒(1.4分钟)完成,而Python脚本大约耗时115秒(1.9分钟)完成。两种方式都耗时过长。如果完成这样一个简单的任务都需要一分钟以上,那么使用Cython的意义何在?请注意,这是我们的问题,并非Cython的问题。.
如前所述,在 Cython 脚本 (.pyx) 中编写 Python 代码是一种改进,但对运行时性能的影响并不显著。我们需要对 Cython 脚本中的 Python 代码进行一些修改。首先,我们需要显式声明所用变量的数据类型。.
将 C 数据类型分配给变量
根据之前的代码,使用了 5 个变量:total、k、t1、t2 和 t。这些变量的数据类型都是由代码隐式推断的,因此耗时较长。为了节省推断数据类型的时间,我们可以使用 C 语言的数据类型定义来指定它们。.
变量 total 的类型是 unsigned long long int。它是整数类型,因为所有数字之和是整数;它是无符号类型,因为和始终为正数。但为什么要使用 long long 类型呢?因为所有数字之和非常大,所以使用 long long 类型来尽可能地增大变量的值。.
变量 k 的数据类型为 int,其余三个变量 t1、t2 和 t 的数据类型为 float。.
import time
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
t1 = time.time()
for k in range(1000000000):
total = total + k
print "Total =", total
t2 = time.time()
t = t2-t1
print("%.100f" % t)请注意,最后一条打印语句中定义的精度设置为 100,而所有这些数字都为零(参见下一张图)。这就是使用 Cython 的预期结果。Python 需要超过 1.9 分钟,而 Cython 几乎不耗时。我甚至不会说它比 Python 快 1000 倍或 100000 倍;我尝试了不同的打印时间精度,仍然没有显示任何数字。.
请注意,您还可以创建一个整型变量来保存传递给 range() 函数的值。这将进一步提高性能。新代码如下所示,其中值存储在整型变量 maxval 中。.
import time
cdef unsigned long long int maxval
cdef unsigned long long int total
cdef int k
cdef float t1, t2, t
maxval=1000000000
t1=time.time()
for k in range(maxval):
total = total + k
print "Total =", total
t2=time.time()
t = t2-t1
print("%.100f" % t)既然我们已经了解了如何使用 Cython 提高 Python 脚本的性能,那么让我们将其应用到 Raspberry Pi (RPi) 上。.
通过个人电脑访问树莓派
如果您是第一次使用树莓派,需要将您的电脑和树莓派连接到网络。您可以将两台设备连接到一台启用了 DHCP(动态主机配置协议)功能的交换机,以便自动分配 IP 地址。网络创建成功后,您就可以通过分配给树莓派的 IPv4 地址访问它了。如何找到树莓派的 IPv4 地址呢?别担心,您可以使用 IP 地址扫描工具。在本教程中,我将使用一款名为 Advanced IP Scanner 的免费应用程序。.
此应用程序的用户界面如下所示。此应用程序接受一系列 IPv4 地址进行搜索,并返回有关活动设备的信息。.
您需要输入本地网络的 IPv4 地址范围。如果您不知道此范围,只需在 Windows 系统中运行 ipconfig 命令(或在 Linux 系统中运行 ifconfig 命令)即可找到计算机的 IPv4 地址(如下图所示)。在我的示例中,分配给计算机 Wi-Fi 适配器的 IPv4 地址为 192.168.43.177,子网掩码为 255.255.255.0。这意味着网络上的 IPv4 地址范围为 192.168.43.1 到 192.168.43.255。如图所示,IPv4 地址 192.168.43.1 分配给了网关。请注意,此范围内的最后一个 IPv4 地址 192.168.43.255 保留用于广播消息。因此,您应该搜索的范围从 192.168.43.2 开始,到 192.168.43.254 结束。.
根据下图所示的扫描结果,分配给 RPi 的 IPv4 地址为 192.168.43.63。该 IPv4 地址可用于建立安全外壳 (SSH) 会话。.
为了建立SSH会话,我将使用一款名为MobaXterm的免费软件。该程序的用户界面如下所示。.
要创建 SSH 会话,只需点击左上角的“会话”按钮。此时将出现一个新窗口,如下图所示。.
在此窗口中,点击左上角的 SSH 按钮,打开如下所示的窗口。只需输入树莓派的 IPv4 地址和用户名(默认为 pi),然后点击“确定”即可开始会话。.
点击“确定”按钮后,将出现一个新窗口,要求您输入密码。默认密码为 raspberrypi。登录后,将出现以下窗口。左侧面板可帮助您轻松浏览 Raspberry Pi 的各个目录。此外,还有一个命令行窗口供您输入命令。.
在树莓派上使用 Cython
创建一个新文件,并将其扩展名更改为 .pyx,即可写入上一个示例中的代码。左侧面板栏提供了创建新文件和目录的选项。您可以使用新建文件图标来简化操作,如下图所示。我在 Raspberry Pi 的根目录中创建了一个名为 test_cython.pyx 的文件。.
双击文件打开,粘贴代码并保存。接下来,我们需要创建 setup.py 文件,其内容与之前讨论的完全相同。之后,我们需要执行以下命令来构建 Cython 脚本。.
python3 setup.py build_ext --inplace
此命令成功执行后,您可以在左侧面板中找到输出文件,如下图所示。请注意,由于我们不再使用 Windows 系统,因此要导入的模块扩展名现在是 .so。.
现在我们激活 Python 并导入模块,如下所示。这里得到的结果与在 PC 上完全相同;耗时几乎为零。.
结果
本教程探讨了如何使用 Cython 来减少运行 Python 脚本的计算时间。我们将向您展示一个使用循环的示例。 为了 我们研究了如何将一个包含 10 亿个数字的 Python 列表中的所有元素相加,并比较了声明变量和不声明变量两种情况下的执行时间。纯 Python 实现这个过程需要近两分钟,而使用 Cython 并声明静态变量后,这个过程几乎瞬间完成。.






















