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-essentialpython3 -m pip install cythondef helloworld(): print("Hello world")创建 setup.py 文件:
from setuptools import setupfrom 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 xcdef double d1, d2
# 声明多个变量cdef: int x double y在 .pyx 文件中,可以使用所有 C 类型(int、double、long 等)。需要特别指出的是 Python 中的不定长整型在 C 中运行时,如果发生溢出,那么将在运行时报 OverflowError 错误。
下面是一个官方示例。
Python 程序:
xxxxxxxxxx# file: integrate_f.pydef 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.pyxdef 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 风格:
xxxxxxxxxxcdef 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 块开始:
xxxxxxxxxxcdef extern from "Rectangle.h" namespace "shapes":这将使 Rectangle 的 C++ 类 def 可用。注意名称空间声明。名称空间仅用于生成对象的完全限定名称,并且可以嵌套(比如,"outer::inner"),甚至可以引用类(比如,"namespace::MyClass" 在 MyClass 上声明静态成员)。
将 Rectangle 类添加到 extern from 块 - 只需从 Rectangle.h 拷贝类名,以及调整 Cython 语法,现在它变成:
xxxxxxxxxxcdef extern from "Rectangle.h" namespace "shapes": cdef cppclass Rectangle:声明 Cython 使用的属性和方法。将这些声明放进名为 Rectangle.pxd 的文件中:
xxxxxxxxxxcdef extern from "Rectangle.cpp": pass
# Declare the class with cdefcdef 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 处理。
使用如下行:
xxxxxxxxxxcdef 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++。
也可以声明栈分配对象,只要它有“默认”构造器:
xxxxxxxxxxcdef 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 = y1Cython 使用无参构造器初始化 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 文件:
xxxxxxxxxxfrom setuptools import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("rect.pyx"))运行:
xxxxxxxxxxpython 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 0C main 函数类似于:
xxxxxxxxxx/* embedded_main.c */
/* This include file is automatically generated by Cython for 'public' functions. */
extern "C" {
intmain(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)查找和捆绑这些依赖项。