Cython是个什么东西

Cython 是一种超集(superset)的 Python 语言,它允许你编写 Python 代码并将其编译为高效的 C 代码,以提升性能。
它的核心目标是加速 Python 代码,同时支持与 C 语言的无缝集成。

Cython 主要用于:

  • 优化 Python 代码性能(比纯 Python 运行得更快)
  • 调用 C/C++ 代码(作为 Python 和 C 之间的桥梁)
  • 创建 Python 扩展模块(.pyd 或 .so 文件,可用于 Python 调用)

Cython通常我们将他读作”赛森”,前面的读音是cy,后面就和python的thon一样了。
需要注意的是Cython并不是Python的标准库,所以是需要如下命令进行安装的:
pip install Cython

Cython所基于Python/C API的,但是初学Cython的时候可以完全不用了解Python/C API
CpythonFlow

使用

很重要的一点:
当在使用Cython将Python变成C/C++代码的时候,需要将代码中的部分类型声明为Cython特定类型注释,用于告诉Cython如何转换。

我们以求Fibonacci数列(斐波那契数列)为例,来看一下Cython的强大。
首先,你要会Python,用纯Python编写求解斐波那契数列的函数代码:

1
2
3
4
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)

pyx文件创建

接下来就开始我们Cython的骚操作了。我们需要创建一个pyx格式的文件,pyx文件就是我们Cython的源文件了,由于Cython是Python的超集,所以pyx文件里面完全可以写Python代码。
将如下Cython代码写入pyx文件,这里就叫example_fib.pyx吧:

1
2
3
4
cpdef int fib(int n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)

你会发现很奇妙的一点,Cython代码几乎和Python代码完全一样,不同的地方则是我们上面提到的重要的一点,需要将代码中的部分类型声明为Cython特定类型。
这里我们则是用cpdef int进行了静态类型声明,定义了一个整型的函数。
注意: cpdef表示python和cython都能调用,而cdef定义的,只能cython调用,可以理解成是私有函数。

编译Cython代码

创建setup.py文件用于编译,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
from setuptools import setup
from Cython.Build import cythonize

setup(
ext_modules=cythonize("example_fib.pyx", language_level="3str")
)

# language_level
# -2编译基于Python-2语法和代码语义.
# -3编译基于Python-3语法和代码语义.
# --3str基于Python-3语法和代码语义进行编译,而不是默认情况下对Python2下的字符串进行unicode编译。
# 3str选项启用Python3语义,但在Python2.x中运行编译代码时,它不会将str类型和无前缀字符串文字更改为unicode

接下来我们运行如下命令进行编译:
python setup.py build_ext
build_ext(build extensions)用于编译和构建 Python C 扩展模块,包括 .pyd(Windows)或 .so(Linux/macOS)文件。
完成编译后你会得到名字为build的目录,以及自动生成的C代码example_fib.c文件。

接下来我们需要找到编译好的扩展名为.pyd的模块(在Linux/macOS平台,扩展名是.so),然后将其复制到我们要使用的目录下。
.pyd位置在build目录下的lib.win-amd64-cpython-310文件夹中,名字类似可能不完全相同,因为我使用的是win x64平台,python版本是3.10的环境,所以名字是这个。
我生成的.pyd文件名为example_fib.cp310-win_amd64.pyd
我们在最上层目录创建一个新的文件夹(当然你可以在任何位置创建,文件夹的名字也是任意的),就叫做fib_calc_test吧,意思就是斐波那契数列计算测试,将example_fib.cp310-win_amd64.pyd复制到该文件夹以供我们python脚本调用。

P.S.: 也可以指定参数 -i/–inplace ,使其将生成的pyd文件自动复制到当前运行命令的目录。
python setup.py build_ext -i

运行测试

fib_calc_test文件夹中创建python脚本fib_calc.py文件用于我们测试运行,内容如下:
这个时候你会发现你的代码编辑器会给你把example_fib库引用报红,不用担心,后面我们会解决这个问题。它并不影响正常运行,只是代码编辑器目前还不认识我们写的东西。
截止到目前,你的fib_calc_test文件夹里面应该有两个文件了,分别是example_fib.cp310-win_amd64.pydfib_calc.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import example_fib
import time


def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)


# 纯Python的计算
python_start_time = time.time()
python_result = fib(41)
python_end_time = time.time()

# 调用刚刚我们写好的Cython的计算
cython_start_time = time.time()
cython_result = example_fib.fib(41)
cython_end_time = time.time()

print("Python计算耗时:", python_end_time - python_start_time, "计算结果: ", python_result)
print("Cython计算耗时:", cython_end_time - cython_start_time, "计算结果: ", cython_result)

我的机器的输出结果如下(耗时单位是秒):

Python计算耗时: 43.804757595062256 计算结果: 165580141
Cython计算耗时: 0.7606198787689209 计算结果: 165580141

具体性能提升了多少,自己计算吧。

自动补全和代码编辑器识别

截止到这里,其实你已经会了Cython的使用,这里的内容其实是代码提示部分。
在上一步的测试过程中你也看到了,如果是跟着我所写的步骤做的,你会发现在你的代码编辑器里面有import相关的报红提示,但是代码却又可以正常运行,这是为什么呢?

原因是这样的,报红是由于代码编辑器无法识别到我们编译好的二进制pyd里面的函数,所以对于编辑器来说,它认为没有这个东西,便给你报红提示。
但是当你运行的时候,其实是python的解释器去主动引入,官方一点的词也就是Python的反射机制
(大白话:由于pyd文件跟我们的测试脚本文件在同一个目录下,在运行的时候数据加载到内存python可以从内存中找到想要的函数定义,所以运行正常。)

因此,我们要解决报红问题,其实也非常简单,我们知道二进制中一定有这个函数,所以我们只需要写点东西,告诉编辑器,有这个函数就ok啦。

pyi文件创建:

.pyi文件(Python Interface Stub), 是python的类型声明文件,Cython生成的.pyd由于没有源码可读,所以代码编辑器也不识别,开发者可以手动创建对应的.pyi文件,来提供相关信息。

创建一个名为example_fib.pyi的文件,并将其放入fib_calc_test文件夹,注意保持跟你写的模块名的一致。内容如下:
截止到目前,你的fib_calc_test文件夹里面应该有三个文件了,分别是example_fib.cp310-win_amd64.pydfib_calc.py以及example_fib.pyi

1
def fib(num: int) -> int: ...

这个时候你在返回你的fib_calc.py文件,你会发现报红提示没有了,并且代码补全也正常了。

P.S.: 好多第三方库,你会发现点进去并不能看到源文件,只能看到一行函数的声明,明白为什么了吧,你看的其实是它的.pyi类型声明文件,而源代码已经被编译成不可读的二进制了

到这里就告一段落了。