作为最流行的语言之一,Python不断地与其他流行语言如C/C++进行比较和对照。对Python最常见的抱怨是它有多慢。你经常会看到基准测试显示C/C++比Python快10倍(或更多)。在今天的教程中,我们将探讨 "Cython",它将使我们在性能方面大大缩小Python与其他语言的差距。
但首先,什么是Cython?
什么是Cython?
Cython是Python编程语言的一个超级集合,它是Python和C/C++之间的一个中间人。简而言之,Cython为我们提供了一种将Python代码编译成C/C++的方法。因此,它并不是真正地直接优化Python,而是将其编译成一种运行速度更快的低层次语言。
这当然意味着Cython永远不可能比C/C++快,相反,由于开销,以及通常代码中会保留一些Python元素(就像只把某些部分转换为C/C++一样),它的速度会慢一点。
但它仍然是一个很好的选择,因为它允许我们使用Python写出快速的代码,而没有太多的麻烦。
关于Cython的另一个有趣的小事,主要的Python库,如NumPy和Pandas已经使用Cython来提高性能。这说明了Cython在业界的应用程度,也应该让你放心,学习Cython真的很值得。
Cython是如何提高性能的?
说Cython只是将Python代码编译成C/C++,有点过于简单化了。作为程序员,我们应该知道Cython到底是如何实现这些性能提升的。
简单地说,Cython应用了多种优化方法。其中大部分与 "键入信息 "有关。这是因为Python是一种动态类型的语言,这意味着变量的类型可以在运行时改变。然而,这是以性能为代价的,在某些情况下会导致性能受到巨大的打击。
Cython为我们提供了为Python变量定义静态类型的能力。因此,我们现在不需要写:
x = 0
我们现在写
cdef int x = 0
就像静态类型语言一样,如果我们试图给Python分配除int 以外的任何东西,这将引发一个错误。
另一个优化是在Cython最初编译Python时进行的。这产生了一个轻微的性能优势,即使你不使用任何Cython语法。其他优化可以从使用C/C++兼容的对象中获得,比如Numpy的数组。
我们不会一次就把所有的各种优化都加进去,而是一次一次地做。这样我们就能监控每一步的性能是如何被影响的。这将帮助你了解哪些优化有更大的作用,最重要的是你将了解Cython是如何提高性能的。
使用Cython编译一个Python程序
在这里,我们有一些代码来生成Python中的斐波那契数列。让我们将这段代码所在的文件命名为 "program1.py"。在本教程的后面,我们将探讨更多的程序。
def fib(n):
n1, n2 = 0, 1
for i in range(1, n + 1):
temp = n1 + n2
n1 = n2
n2 = temp
return n2
我们现在不会做任何改动。我们先来探讨一下如何用Cython来编译这个程序,看看这对性能是否有影响。
设置Cython可能相当烦人,但这将是值得的。
- 你需要做的第一件事是安装Cython,使用
pip install cython或任何同等方法。 - 其次,复制一个你的Python文件,并将扩展名和名称稍微改为"
program1_cy.pyx"。你也可以选择使用相同的名字,但我们这样做是为了基准测试的目的,你将在后面看到。 - 创建一个名为setup.py的文件,在里面粘贴以下代码。
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("program1.pyx",
compiler_directives = { "language_level" : "3"}),
)
第一个参数是用来编译C/C++的文件名,第二个参数定义了我们是使用Python 2还是Python 3。
现在运行以下命令。(确保这一切都发生在同一个目录下)
python setup.py build_ext --inplace
这应该会生成所需的文件。你会注意到一个构建文件夹、一个.so (共享库) 和一个.c 或.cpp 文件。我们的代码现在已经准备好并被编译。让我们试着运行它。在一个新的Python文件中,我们运行下面的代码会给我们的输出。
import program1_cy
print(program1_cy.fib(100))
它将给我们一个值,573147844013817084101 ,这是正确的输出。但是我们怎么知道这在Cython中是否比在Pure Python中快呢?让我们做一个基准测试。
基准测试#1
这是本Python Cython教程的第一个基准测试。我们将建立一个名为test.py 的新文件,在那里我们将编写以下代码。我们将使用timeit库来做基准测试。
import timeit
python= timeit.timeit('program1.fib(10000)',
setup='import program1',number=100)
cython= timeit.timeit('program1_cy.fib(10000)',
setup='import program1_cy',number=100)
print("Python Time: ", python)
print("Cython Time: ", cython)
print(f"{python/cython}x times faster")
我们做的第一个基准测试将是10000,并且将各做100次。(我们进行100次迭代,以消除异常值,使我们的结果更加准确)。
Python Time: 0.3353964
Cython Time: 0.21417430000000004
1.565997414255585x times faster
在这里,我们已经可以看到超过50%的改进。让我们再运行一次。
Python Time: 0.2971565
Cython Time: 0.3253751
0.9132736340303853x times faster
这里我们看到Cython输了。这种情况有时会随机发生,但你会注意到Cython赢得了大多数测试。
让我们增加Fibonacci的nth 数量。这将使结果向Cython倾斜。
对于第100000期。
Python Time: 3.9538857000000003
Cython Time: 1.1979654000000002
3.300500749019963x times faster
在这里,我们看到Cython的速度快了三倍以上而这是在我们这边没有任何改动的情况下。
用Cython添加类型信息
现在我们开始用Cython向Python添加类型信息。通常Python有def 关键字,但是Cython引入了两个新的关键字,叫做cdef 和cpdef 。
cdef 当使用这个声明时,只生成一个C版本的函数/对象。
用cpdef 声明的变量/函数可以在Python和C中使用。有一些例外情况,例如使用C指针时,但我们将在后面的教程中讨论。
那么我们要使用哪一种呢?好吧,从Cython 3.0开始, cpdef 变量不再被支持(因为它们的行为与cdef 变量没有区别)。所以我们将使用cpdef 来表示函数,而使用cdef 来表示变量。
现在让我们添加一些类型信息。(别忘了为函数参数添加类型信息)
cpdef int fib(int n):
cdef int n1 = 0
cdef int n2 = 1
cdef int temp, i
for i in range(1, n + 1):
temp = n1 + n2
n1 = n2
n2 = temp
return n2
下面是我们更新的代码。我们给函数的返回类型是int ,并将所有其他变量也声明为int 。这里的好处是,Python 不需要不断地问自己,"这个变量的类型是什么?"。
这似乎是一个非常小的操作,而且确实如此!但是当你不得不不断地问自己 "这个变量的类型是什么?但是当你必须不断地查询一个变量的类型1000000次时会发生什么呢?由于我们在for循环里面有不止一个变量,你可以把这个数字乘以4-5。
基准# 2
那么,这给我们带来了多大的性能提升呢?
对于第1000个术语:(记得先重新编译)。
Python Time: 0.0008732000000000045
Cython Time: 6.8000000000012495e-06
128.4117647058594x times faster
哇!快了128倍。让我们来试试第10000项的情况。
Python Time: 0.0311598
Cython Time: 5.9000000000003494e-05
528.1322033897992x times faster
快了528倍!更不可思议的是!现在最后一次,对第100000个词。
Python Time: 4.3451835
Cython Time: 0.0015122000000005187
2873.418529294081x times faster
这个结果是我们教程的主要部分,向你展示在Python中使用Cython可以将计算速度提高多少。我们成功地编写了比原来快2000倍的代码。