【翻译】Cython教程8_Cython中使用C++

原创文章,转载请注明: 转载自勤奋的小青蛙
本文链接地址: 【翻译】Cython教程8_Cython中使用C++

概览

Cython现在原生的支持大多数的C++语法。尤其是:

  • 现在可以使用new和del关键字动态分配C ++对象。
  • C ++对象可以进行堆栈分配
  • C ++类可以使用新的关键字cppclass声明。
  • 支持模板化类和函数。
  • 支持重载函数。
  • 支持C ++操作符(例如operator +,operator [],...)的重载。

封装步骤

封装C ++的步骤大致有如下几步:

  1. 在setup.py脚本中或在源文件中本地指定C ++语言。
  2. 使用cdef extern from C++头文件创建一个或多个.pxd文件。在pxd文件中,以cdef cppclass来声明类并且声明公共名称(变量,方法和构造函数)
  3. 通过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))

运行结果:

QQ截图20170322111946

源码可以从此处下载:

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或其他),因为它对调用者的语法没有任何影响。

 

原创文章,转载请注明: 转载自勤奋的小青蛙
本文链接地址: 【翻译】Cython教程8_Cython中使用C++

文章的脚注信息由WordPress的wp-posturl插件自动生成



|2|left
打赏

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: