Cython 是用于 Python 编程语言和扩展的 Cython 编程语言(基于 Pyrex)的静态编译器。它使得为 Python 编写 C 扩展像编写 Python 一样容易。
Cython 语言是 Python 语言的超集,支持调用 C 函数,以及在变量和类属性上声明 C 类型。这使得编译器可以从 Cython 代码生成非常高效的 C 代码。C 代码被生成一次,然后使用 C/C++ 编译器编译。
Cython 是包装外部 C 库、将 CPython 嵌入到现有应用程序,以及用于加速 Python 代码执行的快速 C 模块的理想语言。
比如在 Ubuntu 上,通过如下方式安装 C/C++ 编译器:
sudo apt install -y build-essential
python3 -m pip install cython
def helloworld():
print("Hello world")
创建 setup.py
文件:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("helloworld.pyx")
)
执行如下命令:
python3 setup.py build_ext --inplace
执行完成后,可以在当前目录中,看到 helloworld.c 文件和 .so 或 .pyd 文件。
使用 helloworld 模块:
>>> import helloworld
>>> helloworld.helloworld()
Hello world
>>> import pyximport
>>> pyximport.install()
(None, <pyximport._pyximport3.PyxImportMetaFinder object at 0x7fe8c44086a0>)
>>> import helloworld
>>> helloworld.helloworld()
Hello world
在 Cython 中,添加静态类型声明可以使 Python 代码脱离 Python 的动态特性,生成更高效的 C 代码。在一些情况下,能够使运算效率提升数十倍。
用 cdef
声明变量类型和名称。比如:
# 声明单个变量
cdef int x
cdef double d1, d2
# 声明多个变量
cdef:
int x
double y
在 .pyx
文件中,可以使用所有 C 类型(int、double、long 等)。需要特别指出的是 Python 中的不定长整型在 C 中运行时,如果发生溢出,那么将在运行时报 OverflowError
错误。
下面是一个官方示例。
Python 程序:
xxxxxxxxxx
# file: integrate_f.py
def f(x):
return x ** 2 - x
def integrate_f(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
return s * dx
改为 Cython 程序:
xxxxxxxxxx
# file: integrate_f_cy.pyx
def f(double x):
return x ** 2 - x
def integrate_f(double a, double b, int N):
cdef int i
cdef double s, dx
s = 0
dx = (b - a) / N
for i in range(N):
s += f(a + i * dx)
return s * dx
由于 i
被声明为 C 类型,因此循环体被转化为纯 C 代码。由于 a
、s
和 dx
参与循环,所以对这三个变量进行类型声明,将显著提升效率。而 b
和 N
则不会对效率产生太大影响。
在函数调用频繁的场景,可以直接将函数声明为 C 风格:
xxxxxxxxxx
cdef double f(double x) except? -2:
return x ** 2 - x
其中,cdef
用于声明函数,可以像 C 一样指定返回类型(这里是 double
)。except? -2
的含义是,如果函数调用发生异常,那么返回 -2
,如果函数不会抛出异常,那么可以不写 except
。
cdef
声明的函数不能在 Python 文件中调用,它更像是 Cython 的内部函数,对外不可见。
下面是一个简单的教程。请查阅官网,获取更多信息。
头文件 Rectangle.h:
xxxxxxxxxx
namespace shapes {
class Rectangle {
public:
int x0, y0, x1, y1;
Rectangle();
Rectangle(int x0, int y0, int x1, int y1);
~Rectangle();
int getArea();
void getSize(int* width, int* height);
void move(int dx, int dy);
};
}
实现在 Rectangle.cpp
文件中:
xxxxxxxxxx
namespace shapes {
// Default constructor
Rectangle::Rectangle () {}
// Overloaded constructor
Rectangle::Rectangle (int x0, int y0, int x1, int y1) {
this->x0 = x0;
this->y0 = y0;
this->x1 = x1;
this->y1 = y1;
}
// Destructor
Rectangle::~Rectangle () {}
// Return the area of the rectangle
int Rectangle::getArea () {
return (this->x1 - this->x0) * (this->y1 - this->y0);
}
// Get the size of the rectangle.
// Put the size in the pointer args
void Rectangle::getSize (int *width, int *height) {
(*width) = x1 - x0;
(*height) = y1 - y0;
}
// Move the rectangle by dx dy
void Rectangle::move (int dx, int dy) {
this->x0 += dx;
this->y0 += dy;
this->x1 += dx;
this->y1 += dy;
}
}
包装 C++ 类的过程与包装普通 C 结构体非常相似,只是增加一些内容。下面以创建基本的 cdef extern from
块开始:
xxxxxxxxxx
cdef extern from "Rectangle.h" namespace "shapes":
这将使 Rectangle 的 C++ 类 def 可用。注意名称空间声明。名称空间仅用于生成对象的完全限定名称,并且可以嵌套(比如,"outer::inner"
),甚至可以引用类(比如,"namespace::MyClass"
在 MyClass 上声明静态成员)。
将 Rectangle 类添加到 extern from 块 - 只需从 Rectangle.h 拷贝类名,以及调整 Cython 语法,现在它变成:
xxxxxxxxxx
cdef extern from "Rectangle.h" namespace "shapes":
cdef cppclass Rectangle:
声明 Cython 使用的属性和方法。将这些声明放进名为 Rectangle.pxd
的文件中:
xxxxxxxxxx
cdef extern from "Rectangle.cpp":
pass
# Declare the class with cdef
cdef extern from "Rectangle.h" namespace "shapes":
cdef cppclass Rectangle:
Rectangle() except +
Rectangle(int, int, int, int) except +
int x0, y0, x1, y1
int getArea()
void getSize(int* width, int* height)
void move(int, int)
注意,构造器被声明为“except +”。如果 C++ 代码或初始内存分配由于错误抛出异常,这将使 Cython 安全地抛出适当的 Python 异常。如果没有该声明,起源于 构造器的 C++ 异常,将不被 Cython 处理。
使用如下行:
xxxxxxxxxx
cdef extern from "Rectangle.cpp":
pass
包含 Rectangle.cpp
中的 C++ 代码。也可以向 setuptools 指定 Rectangle.cpp
是一个源文件。为此,可以在 pyx
(非 pxd
)文件的顶部添加该指令:
xxxxxxxxxx
# distutils: sources = Rectangle.cpp
注意,当使用 cdef extern from
时,指定的路径相对于当前文件,但是如果使用 distutils 指令,那么路径相对于 setup.py
。当运行 setup.py
时,如果希望覆盖源代码的路径,那么使用 cythonize()
函数的 aliases
参数。
下面创建一个名为 rect.pyx
的 .pyx
文件,以构建包装器。这里使用 Rectangle
之外的名称,但如果希望为包装器提供与 C++ 类相同的名称,请查看解决命名冲突部分。
在内部,使用 cdef 用 C++ new
语句声明类的变量:
xxxxxxxxxx
# distutils: language = c++
from Rectangle cimport Rectangle
def main():
rec_ptr = new Rectangle(1, 2, 3, 4) # Instantiate a Rectangle object on the heap
try:
rec_area = rec_ptr.getArea()
finally:
del rec_ptr # delete heap allocated object
cdef Rectangle rec_stack # Instantiate a Rectangle object on the stack
行:
xxxxxxxxxx
# distutils: language = c++
告诉 Cython 该 .pyx
文件必须被编译为 C++。
也可以声明栈分配对象,只要它有“默认”构造器:
xxxxxxxxxx
cdef extern from "Foo.h":
cdef cppclass Foo:
Foo()
def func():
cdef Foo foo
...
请参阅 cpp_locals directive 部分,了解避免使用无参/默认构造器的方法。
注意,像 C++ 一样,如果类只有一个构造器,并且无参,那么无需声明它。
常用的编程实践是创建一个 Cython 扩展类型,它持有 C++ 实例作为属性,并且创建一系列转发方法。因此,可以将该 Python 扩展类型实现为:
xxxxxxxxxx
# distutils: language = c++
from Rectangle cimport Rectangle
# Create a Cython extension type which holds a C++ instance
# as an attribute and create a bunch of forwarding methods
# Python extension type.
cdef class PyRectangle:
cdef Rectangle c_rect # Hold a C++ instance which we're wrapping
def __init__(self, int x0, int y0, int x1, int y1):
self.c_rect = Rectangle(x0, y0, x1, y1)
def get_area(self):
return self.c_rect.getArea()
def get_size(self):
cdef int width, height
self.c_rect.getSize(&width, &height)
return width, height
def move(self, dx, dy):
self.c_rect.move(dx, dy)
从 Python 的角度看,该扩展类型看起来就像原生定义的 Rectangle 类。注意,如果希望给出属性访问,那么只需实现一些属性:
xxxxxxxxxx
# distutils: language = c++
from Rectangle cimport Rectangle
cdef class PyRectangle:
cdef Rectangle c_rect
def __init__(self, int x0, int y0, int x1, int y1):
self.c_rect = Rectangle(x0, y0, x1, y1)
def get_area(self):
return self.c_rect.getArea()
def get_size(self):
cdef int width, height
self.c_rect.getSize(&width, &height)
return width, height
def move(self, dx, dy):
self.c_rect.move(dx, dy)
# Attribute access
def x0(self):
return self.c_rect.x0
setter .
def x0(self, x0):
self.c_rect.x0 = x0
# Attribute access
def x1(self):
return self.c_rect.x1
setter .
def x1(self, x1):
self.c_rect.x1 = x1
# Attribute access
def y0(self):
return self.c_rect.y0
setter .
def y0(self, y0):
self.c_rect.y0 = y0
# Attribute access
def y1(self):
return self.c_rect.y1
setter .
def y1(self, y1):
self.c_rect.y1 = y1
Cython 使用无参构造器初始化 cdef 类的 C++ 类属性。如果正在包装的类没有无参构造器,那么必须存储一个指向被包装类的指针,并且手动地分配和释放它。另外,cpp_locals 指令可以避免对指针的需要,并且只在赋值时初始化 C++ 类属性。在 __cinit__
和 __dealloc__
方法中执行这些操作,可以保证在创建和删除 Python 实例时,只调用一次。
xxxxxxxxxx
# distutils: language = c++
from Rectangle cimport Rectangle
cdef class PyRectangle:
cdef Rectangle*c_rect # hold a pointer to the C++ instance which we're wrapping
def __cinit__(self):
self.c_rect = new Rectangle()
def __init__(self, int x0, int y0, int x1, int y1):
self.c_rect.x0 = x0
self.c_rect.y0 = y0
self.c_rect.x1 = x1
self.c_rect.y1 = y1
def __dealloc__(self):
del self.c_rect
为编译 Cython 模块,必须有一个 setup.py
文件:
xxxxxxxxxx
from setuptools import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("rect.pyx"))
运行:
xxxxxxxxxx
python setup.py build_ext --inplace
打开 Python 解释器,测试它:
xxxxxxxxxx
>>> import rect
>>> x0, y0, x1, y1 = 1, 2, 3, 4
>>> rect_obj = rect.PyRectangle(x0, y0, x1, y1)
>>> print(dir(rect_obj))
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'get_area', 'get_size', 'move']
>>> rect_obj.get_size()
(2, 2)
>>> rect_obj.get_area()
4
下面是展示在 Python 3.x 中嵌入 Cython 模块的主要步骤的简单示例。
首先,Cython 模块导出被外部代码调用的 C 函数。注意 say_hello_from_python()
函数被声明为 public
,以将其导出为可以被其它 C 文件(在该示例中为 embedded_main.c
)使用的连接器符号。
xxxxxxxxxx
# embedded.pyx
# The following two lines are for test purposes only, please ignore them.
# distutils: sources = embedded_main.c
# tag: py3only
# tag: no-cpp
TEXT_TO_SAY = 'Hello from Python!'
cdef public int say_hello_from_python() except -1:
print(TEXT_TO_SAY)
return 0
C main
函数类似于:
xxxxxxxxxx
/* embedded_main.c */
/* This include file is automatically generated by Cython for 'public' functions. */
extern "C" {
int
main(int argc, char *argv[])
{
PyObject *pmodule;
wchar_t *program;
program = Py_DecodeLocale(argv[0], NULL);
if (program == NULL) {
fprintf(stderr, "Fatal error: cannot decode argv[0], got %d arguments\n", argc);
exit(1);
}
/* Add a built-in module, before Py_Initialize */
if (PyImport_AppendInittab("embedded", PyInit_embedded) == -1) {
fprintf(stderr, "Error: could not extend in-built modules table\n");
exit(1);
}
/* Pass argv[0] to the Python interpreter */
Py_SetProgramName(program);
/* Initialize the Python interpreter. Required.
If this step fails, it will be a fatal error. */
Py_Initialize();
/* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
pmodule = PyImport_ImportModule("embedded");
if (!pmodule) {
PyErr_Print();
fprintf(stderr, "Error: could not import module 'embedded'\n");
goto exit_with_error;
}
/* Now call into your module code. */
if (say_hello_from_python() < 0) {
PyErr_Print();
fprintf(stderr, "Error in Python code, exception was printed.\n");
goto exit_with_error;
}
/* ... */
/* Clean up after using CPython. */
PyMem_RawFree(program);
Py_Finalize();
return 0;
/* Clean up in the error cases above. */
exit_with_error:
PyMem_RawFree(program);
Py_Finalize();
return 1;
}
}
除自己编写这样的 main
函数外,也可以使用 cython --embed
选项让 Cython 生成一个到模块的 C 文件中。或者使用 cython_freeze 脚本嵌入多个模块。请查阅 embedding demo program 获取完整的示例设置。
注意,应用程序不包含任何外部依赖(包括 Python 标准库模块)。如果希望生成可移植的应用程序,那么建议使用专门的工具(比如 PyInstaller 或 cx_freeze)查找和捆绑这些依赖项。