1. Cython 是什么?

Cython 是用于 Python 编程语言和扩展的 Cython 编程语言(基于 Pyrex)的静态编译器。它使得为 Python 编写 C 扩展像编写 Python 一样容易。

Cython 语言是 Python 语言的超集,支持调用 C 函数,以及在变量和类属性上声明 C 类型。这使得编译器可以从 Cython 代码生成非常高效的 C 代码。C 代码被生成一次,然后使用 C/C++ 编译器编译。

Cython 是包装外部 C 库、将 CPython 嵌入到现有应用程序,以及用于加速 Python 代码执行的快速 C 模块的理想语言。


2. 安装

2.1. 安装 C/C++ 编译器

比如在 Ubuntu 上,通过如下方式安装 C/C++ 编译器:

2.2. 安装 Cython


3. 第一个 Cython 程序:Hello World

3.1. 创建 helloworld.pyx 文件

3.2. 编译

3.2.1. 使用 setuptools

创建 setup.py 文件:

执行如下命令:

执行完成后,可以在当前目录中,看到 helloworld.c 文件和 .so 或 .pyd 文件。

使用 helloworld 模块:

3.2.2. 使用 pyximport

4. 使用静态类型进行加速

在 Cython 中,添加静态类型声明可以使 Python 代码脱离 Python 的动态特性,生成更高效的 C 代码。在一些情况下,能够使运算效率提升数十倍。

4.1. 变量类型声明

cdef 声明变量类型和名称。比如:

.pyx 文件中,可以使用所有 C 类型(int、double、long 等)。需要特别指出的是 Python 中的不定长整型在 C 中运行时,如果发生溢出,那么将在运行时报 OverflowError 错误。

下面是一个官方示例。

Python 程序:

改为 Cython 程序:

由于 i 被声明为 C 类型,因此循环体被转化为纯 C 代码。由于 asdx 参与循环,所以对这三个变量进行类型声明,将显著提升效率。而 bN 则不会对效率产生太大影响。

4.2. 函数类型声明

在函数调用频繁的场景,可以直接将函数声明为 C 风格:

其中,cdef 用于声明函数,可以像 C 一样指定返回类型(这里是 double)。except? -2 的含义是,如果函数调用发生异常,那么返回 -2,如果函数不会抛出异常,那么可以不写 except

cdef 声明的函数不能在 Python 文件中调用,它更像是 Cython 的内部函数,对外不可见。


5. 在 Cython 中使用 C++

下面是一个简单的教程。请查阅官网,获取更多信息。

5.1. 示例 C++ API

头文件 Rectangle.h:

实现在 Rectangle.cpp 文件中:

5.2. 声明 C++ 类接口

包装 C++ 类的过程与包装普通 C 结构体非常相似,只是增加一些内容。下面以创建基本的 cdef extern from 块开始:

这将使 Rectangle 的 C++ 类 def 可用。注意名称空间声明。名称空间仅用于生成对象的完全限定名称,并且可以嵌套(比如,"outer::inner"),甚至可以引用类(比如,"namespace::MyClass" 在 MyClass 上声明静态成员)。

5.2.1. 使用 cdef cppclass 声明类

将 Rectangle 类添加到 extern from 块 - 只需从 Rectangle.h 拷贝类名,以及调整 Cython 语法,现在它变成:

5.2.2. 添加公有属性

声明 Cython 使用的属性和方法。将这些声明放进名为 Rectangle.pxd 的文件中:

注意,构造器被声明为“except +”。如果 C++ 代码或初始内存分配由于错误抛出异常,这将使 Cython 安全地抛出适当的 Python 异常。如果没有该声明,起源于 构造器的 C++ 异常,将不被 Cython 处理。

使用如下行:

包含 Rectangle.cpp 中的 C++ 代码。也可以向 setuptools 指定 Rectangle.cpp 是一个源文件。为此,可以在 pyx(非 pxd)文件的顶部添加该指令:

注意,当使用 cdef extern from 时,指定的路径相对于当前文件,但是如果使用 distutils 指令,那么路径相对于 setup.py。当运行 setup.py 时,如果希望覆盖源代码的路径,那么使用 cythonize() 函数的 aliases 参数。

5.2.3. 使用包装的 C++ 类声明变量

下面创建一个名为 rect.pyx.pyx 文件,以构建包装器。这里使用 Rectangle 之外的名称,但如果希望为包装器提供与 C++ 类相同的名称,请查看解决命名冲突部分。

在内部,使用 cdef 用 C++ new 语句声明类的变量:

行:

告诉 Cython 该 .pyx 文件必须被编译为 C++。

也可以声明栈分配对象,只要它有“默认”构造器:

请参阅 cpp_locals directive 部分,了解避免使用无参/默认构造器的方法。

注意,像 C++ 一样,如果类只有一个构造器,并且无参,那么无需声明它。

5.3. 创建 Cython 包装类

常用的编程实践是创建一个 Cython 扩展类型,它持有 C++ 实例作为属性,并且创建一系列转发方法。因此,可以将该 Python 扩展类型实现为:

从 Python 的角度看,该扩展类型看起来就像原生定义的 Rectangle 类。注意,如果希望给出属性访问,那么只需实现一些属性:

Cython 使用无参构造器初始化 cdef 类的 C++ 类属性。如果正在包装的类没有无参构造器,那么必须存储一个指向被包装类的指针,并且手动地分配和释放它。另外,cpp_locals 指令可以避免对指针的需要,并且只在赋值时初始化 C++ 类属性。在 __cinit____dealloc__ 方法中执行这些操作,可以保证在创建和删除 Python 实例时,只调用一次。

5.4. 编译和导入

为编译 Cython 模块,必须有一个 setup.py 文件:

运行:

打开 Python 解释器,测试它:


6. 在 C/C++ 应用中嵌入 Cython 模块

下面是展示在 Python 3.x 中嵌入 Cython 模块的主要步骤的简单示例。

首先,Cython 模块导出被外部代码调用的 C 函数。注意 say_hello_from_python() 函数被声明为 public,以将其导出为可以被其它 C 文件(在该示例中为 embedded_main.c)使用的连接器符号。

C main 函数类似于:

除自己编写这样的 main 函数外,也可以使用 cython --embed 选项让 Cython 生成一个到模块的 C 文件中。或者使用 cython_freeze 脚本嵌入多个模块。请查阅 embedding demo program 获取完整的示例设置。

注意,应用程序不包含任何外部依赖(包括 Python 标准库模块)。如果希望生成可移植的应用程序,那么建议使用专门的工具(比如 PyInstallercx_freeze)查找和捆绑这些依赖项。