在 Cython 中使用 C ++

原文: http://docs.cython.org/en/latest/src/userguide/wrapping_CPlusPlus.html

概述

Cython 对大多数 C ++语言都有本机支持。特别:

  • 可以使用newdel关键字动态分配 C ++对象。
  • C ++对象可以进行堆栈分配。
  • 可以使用 new 关键字cppclass声明 C ++类。
  • 支持模板化的类和函数。
  • 支持重载功能。
  • 支持重载 C ++运算符(例如 operator +,operator [],…)。

程序概述

包装 C ++文件的一般过程现在可以描述如下:

  • setup.py脚本中指定 C ++语言,或在源文件中指定本地。
  • 使用cdef extern from块和(如果存在)C ++命名空间名称创建一个或多个.pxd文件。在这些块中:
    • 将类声明为cdef cppclass
    • 声明公共名称(变量,方法和构造函数)
  • cimport它们位于一个或多个扩展模块(.pyx文件)中。

一个简单的教程

示例 C ++ API

这是一个很小的 C ++ API,我们将在本文档中将其作为示例。我们假设它将位于名为Rectangle.h的头文件中:

  1. #ifndef RECTANGLE_H
  2. #define RECTANGLE_H
  3. namespace shapes {
  4. class Rectangle {
  5. public:
  6. int x0, y0, x1, y1;
  7. Rectangle();
  8. Rectangle(int x0, int y0, int x1, int y1);
  9. ~Rectangle();
  10. int getArea();
  11. void getSize(int* width, int* height);
  12. void move(int dx, int dy);
  13. };
  14. }
  15. #endif

以及名为Rectangle.cpp的文件中的实现:

  1. #include <iostream>
  2. #include "Rectangle.h"
  3. namespace shapes {
  4. // Default constructor
  5. Rectangle::Rectangle () {}
  6. // Overloaded constructor
  7. Rectangle::Rectangle (int x0, int y0, int x1, int y1) {
  8. this->x0 = x0;
  9. this->y0 = y0;
  10. this->x1 = x1;
  11. this->y1 = y1;
  12. }
  13. // Destructor
  14. Rectangle::~Rectangle () {}
  15. // Return the area of the rectangle
  16. int Rectangle::getArea () {
  17. return (this->x1 - this->x0) * (this->y1 - this->y0);
  18. }
  19. // Get the size of the rectangle.
  20. // Put the size in the pointer args
  21. void Rectangle::getSize (int *width, int *height) {
  22. (*width) = x1 - x0;
  23. (*height) = y1 - y0;
  24. }
  25. // Move the rectangle by dx dy
  26. void Rectangle::move (int dx, int dy) {
  27. this->x0 += dx;
  28. this->y0 += dy;
  29. this->x1 += dx;
  30. this->y1 += dy;
  31. }
  32. }

这非常愚蠢,但应该足以证明所涉及的步骤。

声明 C ++类接口

包装 C ++类的过程与包装普通 C 结构的过程非常类似,只需添加几个。让我们从创建基本cdef extern from块开始:

  1. cdef extern from "Rectangle.h" namespace "shapes":

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

使用 cdef cppclass 声明类

现在,让我们从块中将 Rectangle 类添加到此 extern - 只需从 Rectangle.h 复制类名并调整 Cython 语法,现在它变为:

  1. cdef extern from "Rectangle.h" namespace "shapes":
  2. cdef cppclass Rectangle:

添加公共属性

我们现在需要声明在 Cython 上使用的属性和方法。我们将这些声明放在一个名为Rectangle.pxd的文件中。您可以将其视为可由 Cython 读取的头文件:

  1. cdef extern from "Rectangle.cpp":
  2. pass
  3. # Declare the class with cdef
  4. cdef extern from "Rectangle.h" namespace "shapes":
  5. cdef cppclass Rectangle:
  6. Rectangle() except +
  7. Rectangle(int, int, int, int) except +
  8. int x0, y0, x1, y1
  9. int getArea()
  10. void getSize(int* width, int* height)
  11. void move(int, int)

请注意,构造函数声明为“except +”。如果 C ++代码或初始内存分配由于失败而引发异常,这将使 Cython 安全地引发适当的 Python 异常(见下文)。如果没有此声明,Cython 将不会处理源自构造函数的 C ++异常。

我们使用这些线:

  1. cdef extern from "Rectangle.cpp":
  2. pass

包含来自Rectangle.cpp的 C ++代码。也可以指定Rectangle.cpp为源的 distutils。为此,您可以在.pyx(不是.pxd)文件的顶部添加此指令:

  1. # distutils: sources = Rectangle.cpp

请注意,使用cdef extern from时,指定的路径相对于当前文件,但如果使用 distutils 指令,则路径相对于setup.py。如果要在运行setup.py时发现源的路径,可以使用cythonize()功能的aliases参数。

用包装的 C ++类声明一个 var

我们将创建一个名为rect.pyx.pyx文件来构建我们的包装器。我们使用的是Rectangle以外的名称,但如果您希望为包装器提供与 C ++类相同的名称,请参阅 解决命名冲突 的部分。

在其中,我们使用 cdef 使用 C ++ new语句声明类的 var:

  1. # distutils: language = c++
  2. from Rectangle cimport Rectangle
  3. def main():
  4. rec_ptr = new Rectangle(1, 2, 3, 4) # Instantiate a Rectangle object on the heap
  5. try:
  6. rec_area = rec_ptr.getArea()
  7. finally:
  8. del rec_ptr # delete heap allocated object
  9. cdef Rectangle rec_stack # Instantiate a Rectangle object on the stack

这条线:

  1. # distutils: language = c++

是向 Cython 表明这个.pyx文件必须编译为 C ++。

只要它具有“默认”构造函数,也可以声明堆栈分配的对象:

  1. cdef extern from "Foo.h":
  2. cdef cppclass Foo:
  3. Foo()
  4. def func():
  5. cdef Foo foo
  6. ...

请注意,与 C ++一样,如果类只有一个构造函数并且它是一个无效的,那么就没有必要声明它。

创建 Cython 包装类

此时,我们已经将我们的 pyx 文件的命名空间暴露给 C ++ Rectangle 类型的接口。现在,我们需要从外部 Python 代码访问它(这是我们的全部观点)。

常见的编程实践是创建一个 Cython 扩展类型,它将 C ++实例作为属性保存并创建一堆转发方法。所以我们可以将 Python 扩展类型实现为:

  1. # distutils: language = c++
  2. from Rectangle cimport Rectangle
  3. # Create a Cython extension type which holds a C++ instance
  4. # as an attribute and create a bunch of forwarding methods
  5. # Python extension type.
  6. cdef class PyRectangle:
  7. cdef Rectangle c_rect # Hold a C++ instance which we're wrapping
  8. def __cinit__(self, int x0, int y0, int x1, int y1):
  9. self.c_rect = Rectangle(x0, y0, x1, y1)
  10. def get_area(self):
  11. return self.c_rect.getArea()
  12. def get_size(self):
  13. cdef int width, height
  14. self.c_rect.getSize(&width, &height)
  15. return width, height
  16. def move(self, dx, dy):
  17. self.c_rect.move(dx, dy)

我们终于得到它了。从 Python 的角度来看,这种扩展类型的外观和感觉就像本机定义的 Rectangle 类一样。需要注意的是,如果要提供属性访问权限,可以实现一些属性:

  1. # distutils: language = c++
  2. from Rectangle cimport Rectangle
  3. cdef class PyRectangle:
  4. cdef Rectangle c_rect
  5. def __cinit__(self, int x0, int y0, int x1, int y1):
  6. self.c_rect = Rectangle(x0, y0, x1, y1)
  7. def get_area(self):
  8. return self.c_rect.getArea()
  9. def get_size(self):
  10. cdef int width, height
  11. self.c_rect.getSize(&width, &height)
  12. return width, height
  13. def move(self, dx, dy):
  14. self.c_rect.move(dx, dy)
  15. # Attribute access
  16. @property
  17. def x0(self):
  18. return self.c_rect.x0
  19. @x0.setter
  20. def x0(self, x0):
  21. self.c_rect.x0 = x0
  22. # Attribute access
  23. @property
  24. def x1(self):
  25. return self.c_rect.x1
  26. @x1.setter
  27. def x1(self, x1):
  28. self.c_rect.x1 = x1
  29. # Attribute access
  30. @property
  31. def y0(self):
  32. return self.c_rect.y0
  33. @y0.setter
  34. def y0(self, y0):
  35. self.c_rect.y0 = y0
  36. # Attribute access
  37. @property
  38. def y1(self):
  39. return self.c_rect.y1
  40. @y1.setter
  41. def y1(self, y1):
  42. self.c_rect.y1 = y1

Cython 使用 nullary 构造函数初始化 cdef 类的 C ++类属性。如果要包装的类没有构造函数,则必须存储指向包装类的指针并手动分配和取消分配它。一个方便和安全的地方是 <cite>cinit</cite> 和 <cite>dealloc</cite> 方法,保证在创建和删除 Python 实例时只调用一次。

  1. # distutils: language = c++
  2. from Rectangle cimport Rectangle
  3. cdef class PyRectangle:
  4. cdef Rectangle*c_rect # hold a pointer to the C++ instance which we're wrapping
  5. def __cinit__(self, int x0, int y0, int x1, int y1):
  6. self.c_rect = new Rectangle(x0, y0, x1, y1)
  7. def __dealloc__(self):
  8. del self.c_rect

编译和导入

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

  1. from distutils.core import setup
  2. from Cython.Build import cythonize
  3. setup(ext_modules=cythonize("rect.pyx"))

运行$ python setup.py build_ext --inplace

要测试它,打开 Python 解释器:

  1. >>> import rect
  2. >>> x0, y0, x1, y1 = 1, 2, 3, 4
  3. >>> rect_obj = rect.PyRectangle(x0, y0, x1, y1)
  4. >>> print(dir(rect_obj))
  5. ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
  6. '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
  7. '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
  8. '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'get_area', 'get_size', 'move']

高级 C ++特性

我们在这里描述了上面教程中没有讨论过的所有 C ++特性。

重载

重载非常简单。只需使用不同的参数声明方法并使用其中任何一个:

  1. cdef extern from "Foo.h":
  2. cdef cppclass Foo:
  3. Foo(int)
  4. Foo(bool)
  5. Foo(int, bool)
  6. Foo(int, int)

重载运算符

Cython 使用 C ++命名来重载运算符:

  1. cdef extern from "foo.h":
  2. cdef cppclass Foo:
  3. Foo()
  4. Foo operator+(Foo)
  5. Foo operator-(Foo)
  6. int operator*(Foo)
  7. int operator/(int)
  8. int operator*(int, Foo) # allows 1*Foo()
  9. # nonmember operators can also be specified outside the class
  10. double operator/(double, Foo)
  11. cdef Foo foo = new Foo()
  12. foo2 = foo + foo
  13. foo2 = foo - foo
  14. x = foo * foo2
  15. x = foo / 1
  16. x = foo[0] * foo2
  17. x = foo[0] / 1
  18. x = 1*foo[0]
  19. cdef double y
  20. y = 2.0/foo[0]

请注意,如果有一个 指针 到 C ++对象,则必须进行解除引用以避免执行指针算术而不是对对象本身进行算术运算:

  1. cdef Foo* foo_ptr = new Foo()
  2. foo = foo_ptr[0] + foo_ptr[0]
  3. x = foo_ptr[0] / 2
  4. del foo_ptr

嵌套类声明

C ++允许嵌套类声明。类声明也可以嵌套在 Cython 中:

  1. # distutils: language = c++
  2. cdef extern from "<vector>" namespace "std":
  3. cdef cppclass vector[T]:
  4. cppclass iterator:
  5. T operator*()
  6. iterator operator++()
  7. bint operator==(iterator)
  8. bint operator!=(iterator)
  9. vector()
  10. void push_back(T&)
  11. T& operator[](int)
  12. T& at(int)
  13. iterator begin()
  14. iterator end()
  15. cdef vector[int].iterator iter #iter is declared as being of type vector<int>::iterator

请注意,嵌套类使用cppclass声明但没有cdef,因为它已经是cdef声明部分的一部分。

C ++运算符与 Python 语法不兼容

Cython 尝试使其语法尽可能接近标准 Python。因此,某些 C ++运算符(如 preincrement ++foo或解除引用运算符*foo)不能与 C ++使用相同的语法。 Cython 提供了在特殊模块cython.operator中替换这些运算符的函数。提供的功能是:

  • 用于解除引用的cython.operator.dereferencedereference(foo)将生成 C ++代码*(foo)
  • cython.operator.preincrement用于预增量。 preincrement(foo)将生成 C ++代码++(foo)。类似地predecrementpostincrementpostdecrement
  • 逗号运算符的cython.operator.commacomma(a, b)将生成 C ++代码((a), (b))

这些功能需要被引导。当然,可以使用from ... cimport ... as来获得更短且更易读的功能。例如:from cython.operator cimport dereference as deref

为了完整起见,还值得一提的是cython.operator.address,它也可以写成&foo

模板

Cython 使用括号语法进行模板化。包装 C ++向量的一个简单示例:

  1. # distutils: language = c++
  2. # import dereference and increment operators
  3. from cython.operator cimport dereference as deref, preincrement as inc
  4. cdef extern from "<vector>" namespace "std":
  5. cdef cppclass vector[T]:
  6. cppclass iterator:
  7. T operator*()
  8. iterator operator++()
  9. bint operator==(iterator)
  10. bint operator!=(iterator)
  11. vector()
  12. void push_back(T&)
  13. T& operator[](int)
  14. T& at(int)
  15. iterator begin()
  16. iterator end()
  17. cdef vector[int] *v = new vector[int]()
  18. cdef int i
  19. for i in range(10):
  20. v.push_back(i)
  21. cdef vector[int].iterator it = v.begin()
  22. while it != v.end():
  23. print(deref(it))
  24. inc(it)
  25. del v

可以将多个模板参数定义为列表,例如[T, U, V][int, bool, char]。可以通过写[T, U, V=*]来指示可选的模板参数。如果 Cython 需要显式引用不完整模板实例化的默认模板参数的类型,它将写入MyClass&lt;T, U&gt;::V,因此如果类为其模板参数提供 typedef,则最好在此处使用该名称。

模板函数的定义与类模板类似,模板参数列表位于函数名称后面:

  1. # distutils: language = c++
  2. cdef extern from "<algorithm>" namespace "std":
  3. T max[T](T a, T b)
  4. print(max[long](3, 4))
  5. print(max(1.5, 2.5)) # simple template argument deduction

标准库

C ++标准库的大多数容器已在位于 / Cython / Includes / libcpp 中的 pxd 文件中声明。这些容器是:deque,list,map,pair,queue,set,stack,vector。

例如:

  1. # distutils: language = c++
  2. from libcpp.vector cimport vector
  3. cdef vector[int] vect
  4. cdef int i, x
  5. for i in range(10):
  6. vect.push_back(i)
  7. for i in range(10):
  8. print(vect[i])
  9. for x in vect:
  10. print(x)

/ Cython / Includes / libcpp 中的 pxd 文件也是如何声明 C ++类的好例子。

STL 容器强制执行相应的 Python 内置类型。转换是通过赋值给类型变量(包括类型化函数参数)或通过显式强制转换来触发的,例如:

  1. # distutils: language = c++
  2. from libcpp.string cimport string
  3. from libcpp.vector cimport vector
  4. py_bytes_object = b'The knights who say ni'
  5. py_unicode_object = u'Those who hear them seldom live to tell the tale.'
  6. cdef string s = py_bytes_object
  7. print(s) # b'The knights who say ni'
  8. cdef string cpp_string = <string> py_unicode_object.encode('utf-8')
  9. print(cpp_string) # b'Those who hear them seldom live to tell the tale.'
  10. cdef vector[int] vect = range(1, 10, 2)
  11. print(vect) # [1, 3, 5, 7, 9]
  12. cdef vector[string] cpp_strings = b'It is a good shrubbery'.split()
  13. print(cpp_strings[1]) # b'is'

可以使用以下强制措施:

| Python type =&gt; | C ++类型 | =&GT; Python 类型 || —- | —- | —- || 字节 | 的 std :: string | bytes || 迭代 | 的 std ::矢量 | 名单 || iterable | 的 std ::名单 | list || iterable | 的 std ::设为 | 组 || 可迭代的(len 2) | 的 std ::对 | 元组(len 2) |

所有转换都会创建一个新容器并将数据复制到其中。容器中的物品自动转换成相应的类型,包括递归地转换容器内的容器,例如容器。字符串映射的 C ++向量。

通过for .. in语法(包括列表推导)支持对 stl 容器(或实际上任何具有begin()end()方法的类返回支持递增,解除引用和比较的对象的迭代)的迭代。例如,人们可以写:

  1. # distutils: language = c++
  2. from libcpp.vector cimport vector
  3. def main():
  4. cdef vector[int] v = [4, 6, 5, 10, 3]
  5. cdef int value
  6. for value in v:
  7. print(value)
  8. return [x*x for x in v if x % 2 == 0]

如果未指定循环目标变量,则 类型推断 使用类型*container.begin()的赋值。

注意

支持切片 stl 容器,你可以做for x in my_vector[:5]: ...,但与指针切片不同,它将创建一个临时的 Python 对象并迭代它。因此使迭代非常缓慢。出于性能原因,您可能希望避免切片 C ++容器。

使用默认构造函数简化包装

如果您的扩展类型使用默认构造函数(不传递任何参数)实例化包装的 C ++类,您可以通过将生命周期处理直接绑定到 Python 包装器对象的生命周期来简化生命周期处理。您可以声明一个实例,而不是指针属性:

  1. # distutils: language = c++
  2. from libcpp.vector cimport vector
  3. cdef class VectorStack:
  4. cdef vector[int] v
  5. def push(self, x):
  6. self.v.push_back(x)
  7. def pop(self):
  8. if self.v.empty():
  9. raise IndexError()
  10. x = self.v.back()
  11. self.v.pop_back()
  12. return x

Cython 将自动生成在创建 Python 对象时实例化 C ++对象实例的代码,并在 Python 对象被垃圾回收时删除它。

例外情况

Cython 不能抛出 C ++异常,或者用 try-except 语句捕获它们,但是可以将函数声明为可能引发 C ++异常并将其转换为 Python 异常。例如,

  1. cdef extern from "some_file.h":
  2. cdef int foo() except +

这会将 try 和 C ++错误转换为适当的 Python 异常。根据下表执行转换(从 C ++标识符中省略std::前缀):

| C ++ | 蟒蛇 || —- | —- || 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 || (所有其他人) | RuntimeError |

保留what()消息(如果有)。请注意,C ++ ios_base_failure可以表示 EOF,但是没有足够的信息供 Cython 识别,因此请注意 IO 流上的异常掩码。

  1. cdef int bar() except +MemoryError

这将捕获任何 C ++错误并在其位置引发 Python MemoryError。 (任何 Python 异常在这里都有效。)

  1. cdef int raise_py_error()
  2. cdef int something_dangerous() except +raise_py_error

如果 something_dangerous 引发了 C ++异常,那么将调用 raise_py_error,这允许自定义 C ++到 Python 错误“翻译”。如果 raise_py_error 实际上没有引发异常,则会引发 RuntimeError。

还有一种特殊的形式:

  1. cdef int raise_py_or_cpp() except +*

对于那些可能引发 Python 或 C ++异常的函数。

静态成员方法

如果 Rectangle 类具有静态成员:

  1. namespace shapes {
  2. class Rectangle {
  3. ...
  4. public:
  5. static void do_something();
  6. };
  7. }

你可以使用 Python @staticmethod 装饰器声明它,即:

  1. cdef extern from "Rectangle.h" namespace "shapes":
  2. cdef cppclass Rectangle:
  3. ...
  4. @staticmethod
  5. void do_something()

声明/使用参考文献

Cython 支持使用标准Type&语法声明左值引用。但请注意,没有必要将 extern 函数的参数声明为引用(const 或其他),因为它对调用者的语法没有影响。

auto关键字

虽然 Cython 没有auto关键字,但是没有用cdef显式输入的 Cython 局部变量是从 右侧的类型中推断出所有 的分配(参见infer_types 编译指令 )。在处理返回复杂,嵌套,模板化类型的函数时,这尤其方便,例如:

  1. cdef vector[int] v = ...
  2. it = v.begin()

(当然,对于支持迭代协议的对象,for .. in语法是首选。)

RTTI 和 typeid()

Cython 支持typeid(...)运算符。

来自 cython.operator cimport typeid

typeid(...)运算符返回const type_info &类型的对象。

如果要将 type_info 值存储在 C 变量中,则需要将其存储为指针而不是引用:

  1. from libcpp.typeinfo cimport type_info
  2. cdef const type_info* info = &typeid(MyClass)

如果将无效类型传递给typeid,它将抛出std::bad_typeid异常,该异常在 Python 中转换为TypeError异常。

libcpp.typeindex中提供了另一个与 C ++ 11 兼容的 RTTI 相关类std::type_index

在 setup.py 中指定 C ++语言

可以在setup.py文件中声明它们,而不是在源文件中指定语言和源:

  1. from distutils.core import setup
  2. from Cython.Build import cythonize
  3. setup(ext_modules = cythonize(
  4. "rect.pyx", # our Cython source
  5. sources=["Rectangle.cpp"], # additional source file(s)
  6. language="c++", # generate C++ code
  7. ))

Cython 将生成并编译rect.cpp文件(来自rect.pyx),然后它将编译Rectangle.cppRectangle类的实现)并将两个目标文件一起链接到 Linux 上的rect.sorect.pyd在 Windows 上,然后可以使用import rect在 Python 中导入(如果忘记链接Rectangle.o,则在 Python 中导入库时将丢失符号)。

请注意,language选项对传递给cythonize()的用户提供的 Extension 对象没有影响。它仅用于按文件名找到的模块(如上例所示)。

Cython 版本中最大 0.21 的cythonize()功能无法识别language选项,需要将其指定为描述扩展名的Extension选项,然后由cythonize()处理,如下所示:

  1. from distutils.core import setup, Extension
  2. from Cython.Build import cythonize
  3. setup(ext_modules = cythonize(Extension(
  4. "rect", # the extension name
  5. sources=["rect.pyx", "Rectangle.cpp"], # the Cython source and
  6. # additional C++ source files
  7. language="c++", # generate and compile C++ code
  8. )))

选项也可以直接从源文件传递,这通常是可取的(并覆盖任何全局选项)。从版本 0.17 开始,Cython 还允许以这种方式将外部源文件传递到cythonize()命令。这是一个简化的 setup.py 文件:

  1. from distutils.core import setup
  2. from Cython.Build import cythonize
  3. setup(
  4. name = "rectangleapp",
  5. ext_modules = cythonize('*.pyx'),
  6. )

在.pyx 源文件中,将其写入第一个注释块,在任何源代码之前,以 C ++模式编译它并将其静态链接到Rectangle.cpp代码文件:

  1. # distutils: language = c++
  2. # distutils: sources = Rectangle.cpp

Note

使用 distutils 指令时,路径相对于 distutils 运行的工作目录(通常是setup.py所在的项目根目录)。

要手动编译(例如使用make),可以使用cython命令行实用程序生成 C ++ .cpp文件,然后将其编译为 python 扩展。使用--cplus选项打开cython命令的 C ++模式。

警告和限制

访问仅限 C 的函数

每当生成 C ++代码时,Cython 都会生成函数的声明和调用,假设这些函数是 C ++(即,未声明为extern "C" {...}。如果 C 函数具有 C ++入口点,这是可以的,但如果它们只是 C,那么你将遇到障碍。如果你有一个 C ++ Cython 模块需要调用 pure-C 函数,你需要编写一个小的 C ++ shim 模块:

  • 包括 extern“C”块中所需的 C 头
  • 包含 C ++中的最小转发函数,每个函数都调用相应的 pure-C 函数

C ++左值

C ++允许返回引用的函数为 left-values。 Cython 目前不支持此功能。 cython.operator.dereference(foo)也不被视为左值。