概览
Cython现在原生的支持大多数的C++语法。尤其是:
- 现在可以使用new和del关键字动态分配C ++对象。
- C ++对象可以进行堆栈分配
- C ++类可以使用新的关键字cppclass声明。
- 支持模板化类和函数。
- 支持重载函数。
- 支持C ++操作符(例如operator +,operator [],...)的重载。
封装步骤
封装C ++的步骤大致有如下几步:
- 在setup.py脚本中或在源文件中本地指定C ++语言。
- 使用cdef extern from C++头文件创建一个或多个.pxd文件。在pxd文件中,以cdef cppclass来声明类并且声明公共名称(变量,方法和构造函数)
- 通过cimport引入pxd文件,进行pxd的实现代码,也就是pyx文件。
一个简单的封装教程示例
我们需要封装一个C++的api接口。
Rectangle.h:
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:
#include "Rectangle.h" namespace shapes { Rectangle::Rectangle() { } Rectangle::Rectangle(int X0, int Y0, int X1, int Y1) { x0 = X0; y0 = Y0; x1 = X1; y1 = Y1; } Rectangle::~Rectangle() { } int Rectangle::getArea() { return (x1 - x0) * (y1 - y0); } void Rectangle::getSize(int *width, int *height) { (*width) = x1 - x0; (*height) = y1 - y0; } void Rectangle::move(int dx, int dy) { x0 += dx; y0 += dy; x1 += dx; y1 += dy; } }
上述两个文件,应该是非常简单的C++代码。我们针对这个类进行Cython封装。
我们按照上面讲述的4个步骤进行逐个封装
在setup.py中指定C++语言
从setup.py脚本创建Cython代码的最好方法是使用cythonize()函数。为了让Cython用distutils生成和编译C ++代码,你只需要传递选项language =“c ++”:
from distutils.core import setup from Cython.Build import cythonize setup(ext_modules = cythonize( "rect.pyx", # our Cython source sources=["Rectangle.cpp"], # additional source file(s) language="c++", # generate C++ code ))
从上面的配置,我们应该可以看出来Cython将生成并编译rect.cpp文件(从rect.pyx),然后它将编译Rectangle.cpp(Rectangle类的实现)并将两个对象文件链接到rect.so中,然后你可以使用import rect导入Python(如果你忘记链接Rectangle.o,在Python代码中,你会得到丢失的符号的错误)。
请注意,语言选项对传递到cythonize()的用户提供的扩展对象没有影响。它仅用于通过文件名找到的模块(如上例所示)。
下面斜体内容介绍的setup.py方法均为可选方法,不建议使用:
在Cython版本低于0.21的cythonize()函数不能识别语言选项,它需要被指定为一个扩展描述您的扩展的选项,然后由cythonize()处理如下:
from distutils.core import setup, Extension from Cython.Build import cythonize setup(ext_modules = cythonize(Extension( "rect", # the extension name sources=["rect.pyx", "Rectangle.cpp"], # the Cython source and # additional C++ source files language="c++", # generate and compile C++ code )))
选项也可以直接从源文件传递,这通常是首选(并覆盖任何全局选项)。从版本0.17开始,Cython还允许以这种方式将外部源文件传递到cythonize()命令中。以下是一个简化的setup.py文件:
from distutils.core import setup from Cython.Build import cythonize setup( name = "rectangleapp", ext_modules = cythonize('*.pyx'), )
在.pyx源文件中,将下面两行写入第一个注释块,一定要在顶部,在任何源代码之前写下面两行,在C++模式下编译它,并将其静态链接到Rectangle.cpp代码文件:
# distutils: language = c++ # distutils: sources = Rectangle.cpp
要手动编译(例如使用make),cython命令行实用程序可以用于生成一个C++ .cpp文件,然后将其编译为一个python扩展。使用--cplus选项打开cython命令的C++模式。
编写pxd文件(声明C++类接口)
这一步骤和封装C代码Struct大致相同,只不过我们需要额外加一些C++相关的属性。我们从 cdef extern from 开始封装
cdef extern from "Rectangle.h" namespace "shapes":
通过上述代码,我们引入了C++的头文件。注意命名空间的写法。
命名空间仅仅用于创建对象的完全限定名,并且可以嵌套(例如“outer :: inner”)甚至引用类(例如“namespace :: MyClass”来声明MyClass上的静态成员)。
然后紧接着,我们在cdef extern from 语句之下定义我们的Cython类,并且添加属性:
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处理。
定义好了pxd文件,下一步我们便要实现pxd里定义的方法。
实现pxd类(在pyx文件实现Cython封装类的方法)
这一步骤是重点,我们终于要在pyx文件里实现C++暴露出来的接口了,也就是要实现pxd文件里的接口方法了。
通常我们的做法是创建一个Cython扩展类,它保存一个C ++类实例作为一个属性,并创建一堆转发方法。所以我们可以实现pyx代码是:
cdef class PyRectangle: cdef Rectangle c_rect # hold a C++ instance which we're wrapping def __cinit__(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)
这样,我们就完成了C++的封装。而且从Python的开发角度来看,这个扩展类型看起来和感觉就像一个本地定义的Rectangle类。
需要注意的是,如果我们需要额外的属性设置方法,可以自己再添加,比如:
@property def x0(self): return self.c_rect.x0 @x0.setter def x0(self): def __set__(self, x0): self.c_rect.x0 = x0 ...
查看我们的pyx代码,Cython使用默认构造函数初始化C++类实例,如上面代码中:
<span class="k">cdef</span> <span class="kt">Rectangle</span> <span class="nf">c_rect</span>
。如果你包装的类不想用默认的构造函数,还想传入一些参数,那么你就可以存储一个指向包装类的指针,并手动分配和释放它。如下所示:
cdef class PyRectangle: cdef Rectangle* c_rect # hold a pointer to the C++ instance which we're wrapping def __cinit__(self, int x0, int y0, int x1, int y1): self.c_rect = new Rectangle(x0, y0, x1, y1) def __dealloc__(self): del self.c_rect ...
到此为止,我们就完成了封装C++类的过程,下一步直接进行编译即可使用封装完成的类了。
最终的代码:
rect.pxd:
# distutils: language = c++ # distutils: sources = Rectangle.cpp cdef extern from "Rectangle.h": 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)
rect.pyx:
# distutils: language = c++ # distutils: sources = Rectangle.cpp cdef class PyRectangle: cdef Rectangle c_rect # hold a C++ instance which we're wrapping def __cinit__(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)
具体使用过程:
1:编译:
python setup.py build_ext --inplace
2:测试:
rect_test.py:
import rect if __name__ == '__main__': pyRect = rect.PyRectangle(100, 100, 300, 500) width, height = pyRect.get_size() print("size: width = %d, height = %d" % (width, height))
运行结果:
源码可以从此处下载:
Cython封装C++代码示例下载
一些更高级的C++特性封装
重载函数
重载在Cython里比较简单,比如Cython里如下重载代码,直接使用即可:
cdef extern from "Foo.h": cdef cppclass Foo: Foo(int) Foo(bool) Foo(int, bool) Foo(int, int)
运算符重载
Cython使用C ++命名重载操作符,如下所示:
cdef extern from "foo.h": cdef cppclass Foo: Foo() Foo operator+(Foo) Foo operator-(Foo) int operator*(Foo) int operator/(int) cdef Foo foo = new Foo() foo2 = foo + foo foo2 = foo - foo x = foo * foo2 x = foo / 1
注意:如果有指向C++对象的指针,则必须完成取消指针引用,以避免运算的是指针而不是对象本身。
如下所示:
cdef Foo* foo_ptr = new Foo() foo = foo_ptr[0] + foo_ptr[0] x = foo_ptr[0] / 2 del foo_ptr
嵌套类
C++允许嵌套类声明。类声明也可以嵌套在Cython中。
cdef extern from "<vector>" namespace "std": cdef cppclass vector[T]: cppclass iterator: T operator*() iterator operator++() bint operator==(iterator) bint operator!=(iterator) vector() void push_back(T&) T& operator[](int) T& at(int) iterator begin() iterator end() cdef vector[int].iterator iter #iter is declared as being of type vector<int>::iterator
请注意,嵌套类使用cppclass声明但不使用cdef。
模板
Cython使用括号语法进行模板化。下面是一个包装C ++ Vector的简单示例:
# import dereference and increment operators from cython.operator cimport dereference as deref, preincrement as inc cdef extern from "<vector>" namespace "std": cdef cppclass vector[T]: cppclass iterator: T operator*() iterator operator++() bint operator==(iterator) bint operator!=(iterator) vector() void push_back(T&) T& operator[](int) T& at(int) iterator begin() iterator end() cdef vector[int] *v = new vector[int]() cdef int i for i in range(10): v.push_back(i) cdef vector[int].iterator it = v.begin() while it != v.end(): print deref(it) inc(it) del v
多个模板参数可以定义为列表,如[T,U,V]或[int,bool,char]。可以通过写入[T,U,V = *]来指示可选的模板参数。如果Cython需要显式引用不完整模板实例化的默认模板参数的类型,它将编写MyClass <T,U> :: V,所以如果类为其模板参数提供了typedef,那么最好在这里使用该名称。
模板函数的定义与类模板类似,模板参数列表跟随函数名称:
cdef extern from "<algorithm>" namespace "std": T max[T](T a, T b) print max[long](3, 4) print max(1.5, 2.5) # simple template argument deduction
C++标准库封装
大多数C ++标准库的容器已在位于/ Cython / Includes / libcpp的pxd文件中声明。这些容器是:deque,list,map,pair,queue,set,stack,vector。
例如:
from libcpp.vector cimport vector cdef vector[int] vect cdef int i for i in range(10): vect.push_back(i) for i in range(10): print vect[i]
在目录/Cython/Includes/libcpp中的pxd文件也可以作为一个很好的例子来说明如何声明C++类。
自从Cython 0.17以来,STL容器从相应的Python内建类型中强制转换。转换通过对类型化变量(包括类型化函数参数)的赋值或显式转换触发,例如:
from libcpp.string cimport string from libcpp.vector cimport vector cdef string s = py_bytes_object print(s) cpp_string = <string> py_unicode_object.encode('utf-8') cdef vector[int] vect = xrange(1, 10, 2) print(vect) # [1, 3, 5, 7, 9] cdef vector[string] cpp_strings = b'ab cd ef gh'.split() print(cpp_strings[1]) # b'cd'
以下强制可用:
Python type => | C++ type | => Python type |
---|---|---|
bytes | std::string | bytes |
iterable | std::vector | list |
iterable | std::list | list |
iterable | std::set | set |
iterable (len 2) | std::pair | tuple (len 2) |
所有转换都会创建一个新的容器并将数据复制到该容器中。容器中的项目会自动转换为相应的类型,包括递归转换容器内的容器,例如字符串map转换为vector。
支持在stl容器(或实际上任何类与begin()和end()方法返回支持递增,取消引用和比较的对象)通过for语法支持。包括list解析。例如如下代码:
cdef vector[int] v = ... for value in v: f(value) return [x*x for x in v if x % 2 == 0]
如果循环目标变量未指定,则类型* container.begin()的分配将用于类型推断。
使用默认构造函数简化包装
如果您的扩展类型使用默认构造函数(不传递任何参数)来实例化包装的C ++类,则可以通过将其直接绑定到Python包装器对象的生命周期来简化生命周期处理。取代声明一个指针,您可以声明一个实例:
cdef class VectorStack: cdef vector[int] v def push(self, x): self.v.push_back(x) def pop(self): if self.v.empty(): raise IndexError() x = self.v.back() self.v.pop_back() return x
当Python对象被创建时,Cython将自动生成实例化C ++对象实例的代码,并在Python对象被垃圾回收时将其删除。
异常Exception处理
Cython不能抛出C ++异常,或者使用try-except语句来捕获它们,但是有可能声明一个函数可能引发C ++异常并将其转换为Python异常。例如,
cdef extern from "some_file.h": cdef int foo() except +
这将将try和C ++错误翻译成适当的Python异常。根据下表执行翻译(C ++标识符中省略了std ::前缀):
C++ | Python |
---|---|
bad_alloc |
MemoryError |
bad_cast |
TypeError |
bad_typeid |
TypeError |
domain_error |
ValueError |
invalid_argument |
ValueError |
ios_base::failure |
IOError |
out_of_range |
IndexError |
overflow_error |
OverflowError |
range_error |
ArithmeticError |
underflow_error |
ArithmeticError |
(all others) | RuntimeError |
what()消息(如果有)保留。请注意,C ++ ios_base_failure可以表示EOF,但是没有足够的信息为Cython识别,所以请注意IO流上的异常掩码。
cdef int bar() except +MemoryError
这将捕获任何C ++错误,并在其中引发Python MemoryError。 (任何Python异常在此处都有效)
cdef int raise_py_error() cdef int something_dangerous() except +raise_py_error
如果有不可预知的错误代码引发了一个C ++异常,那么raise_py_error将被调用,这允许一个人自定义C ++到Python的错误“translations”。如果raise_py_error实际上并不引发一个异常,则会引发一个RuntimeError。
静态成员方法
如果最开头我们定义的C++类Rectangle类具有静态成员:
namespace shapes { class Rectangle { ... public: static void do_something(); }; }
您可以使用Python @staticmethod装饰器声明它,即:
cdef extern from "Rectangle.h" namespace "shapes": cdef cppclass Rectangle: ... @staticmethod void do_something()
声明/使用引用
Cython支持使用标准的Type&语法来声明lvalue引用。但是请注意,没有必要将extern函数的参数声明为引用(const或其他),因为它对调用者的语法没有任何影响。
文章的脚注信息由WordPress的wp-posturl插件自动生成