在 Cython 中使用 C++

概述

Cython 原生支持大多数 C++ 语言。具体来说

  • C++ 对象可以使用 newdel 关键字进行 动态分配

  • C++ 对象可以进行 栈分配

  • C++ 类可以使用新的关键字 cppclass 声明。

  • 支持模板类和函数。

  • 支持重载函数。

  • 支持 C++ 运算符的重载(例如 operator+operator[] 等)。

过程概述

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

  • setup.py 脚本中或源文件中的本地位置指定 C++ 语言。

  • 创建一个或多个 .pxd 文件,其中包含 cdef extern from 块(如果存在)以及 C++ 命名空间名称。在这些块中

    • 将类声明为 cdef cppclass

    • 声明公共名称(变量、方法和构造函数)

  • 在一个或多个扩展模块(.pyx 文件)中 cimport 它们。

一个简单的教程

一个 C++ API 示例

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

#ifndef RECTANGLE_H
#define 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);
    };
}

#endif

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

#include <iostream>
#include "Rectangle.h"

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++ 类的过程与包装普通 C 结构体的过程非常相似,只是增加了一些内容。让我们从这里开始,创建基本的 cdef extern from

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

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

使用 cdef cppclass 声明类

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

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

添加公共属性

现在我们需要声明在 Cython 上使用的属性和方法。我们将这些声明放在一个名为 Rectangle.pxd 的文件中。你可以把它看作一个 Cython 可读的头文件。

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 处理。

我们使用以下行

cdef extern from "Rectangle.cpp":
    pass

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

# distutils: sources = Rectangle.cpp

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

用包装的 C++ 类声明一个变量

我们将创建一个名为 rect.pyx.pyx 文件来构建我们的包装器。我们使用一个与 Rectangle 不同的名称,但如果你更喜欢给包装器和 C++ 类相同的名称,请参阅关于 解决命名冲突 的部分。

在里面,我们使用 cdef 来声明一个使用 C++ new 语句的类的变量

# 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

这行

# distutils: language = c++

是为了告诉 Cython,这个 .pyx 文件必须编译成 C++。

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

cdef extern from "Foo.h":
    cdef cppclass Foo:
        Foo()

def func():
    cdef Foo foo
    ...

有关避免需要零参数/默认构造函数的方法,请参阅 cpp_locals 指令 部分。

请注意,与 C++ 一样,如果类只有一个构造函数,并且它是一个零参数构造函数,则不需要声明它。

创建 Cython 包装器类

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

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

# 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 类。应该注意的是,如果你想提供属性访问,你可以简单地实现一些属性

# 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
    @property
    def x0(self):
        return self.c_rect.x0
    @x0.setter
    def x0(self, x0):
        self.c_rect.x0 = x0

    # Attribute access
    @property
    def x1(self):
        return self.c_rect.x1
    @x1.setter
    def x1(self, x1):
        self.c_rect.x1 = x1

    # Attribute access
    @property
    def y0(self):
        return self.c_rect.y0
    @y0.setter
    def y0(self, y0):
        self.c_rect.y0 = y0

    # Attribute access
    @property
    def y1(self):
        return self.c_rect.y1
    @y1.setter
    def y1(self, y1):
        self.c_rect.y1 = y1

Cython 使用零参数构造函数初始化 cdef 类的 C++ 类属性。如果你要包装的类没有零参数构造函数,你必须存储指向包装类的指针,并手动分配和释放它。或者,cpp_locals 指令 避免了对指针的需要,并且只在将 C++ 类属性分配给它时才初始化它。一个方便且安全的地方是在 __cinit____dealloc__ 方法中,这些方法保证在创建和删除 Python 实例时只调用一次。

# 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 文件

from setuptools import setup

from Cython.Build import cythonize

setup(ext_modules=cythonize("rect.pyx"))

运行 $ python setup.py build_ext --inplace

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

>>> 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']

高级 C++ 特性

这里我们将介绍上面教程中没有讨论的所有 C++ 特性。

重载

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

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)
        int operator*(int, Foo) # allows 1*Foo()
    # nonmember operators can also be specified outside the class
    double operator/(double, Foo)


cdef Foo foo = new Foo()

foo2 = foo + foo
foo2 = foo - foo

x = foo * foo2
x = foo / 1

x = foo[0] * foo2
x = foo[0] / 1
x = 1*foo[0]

cdef double y
y = 2.0/foo[0]

请注意,如果有人拥有指向 C++ 对象的指针,则必须进行解引用以避免执行指针算术而不是对对象本身进行算术运算

cdef Foo* foo_ptr = new Foo()
foo = foo_ptr[0] + foo_ptr[0]
x = foo_ptr[0] / 2

del foo_ptr

嵌套类声明

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

# distutils: language = c++

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 声明,因为它已经是 cdef 声明部分的一部分。

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

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

  • cython.operator.dereference 用于解引用。 dereference(foo) 将生成 C++ 代码 *(foo)

  • cython.operator.preincrement 用于前增量。 preincrement(foo) 将生成 C++ 代码 ++(foo)。类似地,对于 predecrementpostincrementpostdecrement

  • cython.operator.comma 用于逗号运算符。 comma(a, b) 将生成 C++ 代码 ((a), (b))

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

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

模板

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

# distutils: language = c++

# 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,最好在这里使用该名称。

模板函数的定义类似于类模板,模板参数列表位于函数名之后

# distutils: language = c++

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++ 标准库中的大多数容器都已在位于 /Cython/Includes/libcpp 中的 pxd 文件中声明。这些容器是:deque、list、map、pair、queue、set、stack、vector。

例如

# distutils: language = c++

from libcpp.vector cimport vector

cdef vector[int] vect
cdef int i, x

for i in range(10):
    vect.push_back(i)

for i in range(10):
    print(vect[i])

for x in vect:
    print(x)

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

STL 容器可以从相应的 Python 内置类型转换而来,也可以转换为相应的 Python 内置类型。转换是在赋值给类型化变量(包括类型化函数参数)或显式转换时触发的,例如:

# cython: language_level=3
# distutils: language=c++

from libcpp.complex cimport complex, conj
from libcpp.string cimport string
from libcpp.vector cimport vector

py_bytes_object = b'The knights who say ni'
py_unicode_object = u'Those who hear them seldom live to tell the tale.'

cdef string s = py_bytes_object
print(s)  # b'The knights who say ni'

cdef string cpp_string = <string> py_unicode_object.encode('utf-8')
print(cpp_string)  # b'Those who hear them seldom live to tell the tale.'

cdef vector[int] vect = range(1, 10, 2)
print(vect)  # [1, 3, 5, 7, 9]

cdef vector[string] cpp_strings = b'It is a good shrubbery'.split()
print(cpp_strings[1])   # b'is'

# creates a python object, then convert it to C++ complex
complex_val = 1+2j
cdef complex[double] c_value1 = complex_val
print(c_value1)  # (1+2j)

# transforms a C++ object to another one without Python conversion
cdef complex[double] c_value2 = conj(c_value1)
print(c_value2)  # (1-2j)

以下转换可用

Python 类型 =>

C++ 类型

=> Python 类型

字节

std::string

字节

可迭代对象

std::vector

列表

可迭代对象

std::list

列表

可迭代对象

std::set

集合

可迭代对象

std::unordered_set

集合

映射

std::map

字典

映射

std::unordered_map

字典

可迭代对象(长度为 2)

std::pair

元组(长度为 2)

复数

std::complex

复数

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

请注意,转换确实有一些陷阱,这些陷阱在 故障排除部分 中有详细说明。

通过 for .. in 语法(包括列表推导)支持对 stl 容器(或任何具有 begin()end() 方法的类,这些方法返回支持递增、解引用和比较的对象)进行迭代。例如,可以编写

# distutils: language = c++

from libcpp.vector cimport vector

def main():
    cdef vector[int] v = [4, 6, 5, 10, 3]

    cdef int value
    for value in v:
        print(value)

    return [x*x for x in v if x % 2 == 0]

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

注意

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

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

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

# distutils: language = c++

from libcpp.vector cimport vector


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

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

异常

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

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

这将把 try 和 C++ 错误转换为相应的 Python 异常。转换是根据以下表格执行的(std:: 前缀已从 C++ 标识符中省略)

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

(所有其他)

RuntimeError

如果存在,则保留 what() 消息。请注意,C++ ios_base_failure 可以表示 EOF,但没有足够的信息让 Cython 区分这一点,因此请注意 IO 流上的异常掩码。

cdef int bar() except +MemoryError

这将捕获任何 C++ 错误,并改为引发 Python MemoryError。(任何 Python 异常在这里都是有效的。)

Cython 还支持使用自定义异常处理程序。这是一个大多数用户不需要的高级功能,但对于那些需要它的用户,以下是一个完整的示例

cdef int raise_py_error()
cdef int something_dangerous() except +raise_py_error

如果 something_dangerous 抛出 C++ 异常,则会调用 raise_py_error,这允许进行自定义的 C++ 到 Python 错误“转换”。如果 raise_py_error 实际上没有抛出异常,则会抛出 RuntimeError。这种方法也可以用来管理使用 Python C API 创建的自定义 Python 异常。

# raising.pxd
cdef extern from "Python.h" nogil:
    ctypedef struct PyObject

cdef extern from *:
    """
    #include <Python.h>
    #include <stdexcept>
    #include <ios>

    PyObject *CustomLogicError;

    void create_custom_exceptions() {
        CustomLogicError = PyErr_NewException("raiser.CustomLogicError", NULL, NULL);
    }

    void custom_exception_handler() {
        try {
            if (PyErr_Occurred()) {
                ; // let the latest Python exn pass through and ignore the current one
            } else {
                throw;
            }
        }  catch (const std::logic_error& exn) {
            // Add mapping of std::logic_error -> CustomLogicError
            PyErr_SetString(CustomLogicError, exn.what());
        } catch (...) {
            PyErr_SetString(PyExc_RuntimeError, "Unknown exception");
        }
    }

    class Raiser {
        public:
            Raiser () {}
            void raise_exception() {
                throw std::logic_error("Failure");
            }
    };
    """
    cdef PyObject* CustomLogicError
    cdef void create_custom_exceptions()
    cdef void custom_exception_handler()

    cdef cppclass Raiser:
        Raiser() noexcept
        void raise_exception() except +custom_exception_handler


# raising.pyx
create_custom_exceptions()
PyCustomLogicError = <object> CustomLogicError


cdef class PyRaiser:
    cdef Raiser c_obj

    def raise_exception(self):
        self.c_obj.raise_exception()

上面的示例利用了 Cython 的能力,可以在 pxd 文件中包含 逐字 C 代码 来创建一个新的 Python 异常类型 CustomLogicError,并使用 custom_exception_handler 函数将其映射到标准 C++ std::logic_error。这里使用标准异常类没有什么特别之处,std::logic_error 可以很容易地被替换为在这个文件中定义的一些新的 C++ 异常类型。Raiser::raise_exception 被标记为 +custom_exception_handler,表示只要抛出异常,就会调用这个函数。相应的 Python 函数 PyRaiser.raise_exception 在被调用时会抛出 CustomLogicError。定义 PyCustomLogicError 允许其他代码捕获这个异常,如下所示

try:
    PyRaiser().raise_exception()
except PyCustomLogicError:
    print("Caught the exception")

在定义自定义异常处理程序时,通常最好也包含处理 Cython 通常处理的所有标准异常的逻辑,如上表所示。此标准异常处理程序的代码可以在这里找到 here

还有一种特殊形式

cdef int raise_py_or_cpp() except +*

对于那些可能抛出 Python 或 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& 语法声明左值引用。但是,请注意,没有必要将外部函数的参数声明为引用(const 或其他),因为它对调用者的语法没有影响。

作用域枚举

Cython 在 C++ 模式下支持作用域枚举 (enum class)

cdef enum class Cheese:
    cheddar = 1
    camembert = 2

与“普通”枚举一样,您可以将枚举器作为类型的属性访问。但是,与普通枚举不同,枚举器对封闭范围不可见

cdef Cheese c1 = Cheese.cheddar  # OK
cdef Cheese c2 = cheddar  # ERROR!

可以选择指定作用域枚举的底层类型。当声明具有底层类型的外部作用域枚举时,这一点尤其重要

cdef extern from "Foo.h":
    cdef enum class Spam(unsigned int):
        x = 10
        y = 20
        ...

将枚举类声明为 cpdef 将创建一个 PEP 435-style Python 包装器。

auto 关键字

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

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

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

RTTI 和 typeid()

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

from cython.operator cimport typeid

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

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

from libcpp.typeinfo cimport type_info
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 文件中声明它们。

from setuptools 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.cppRectangle 类的实现)并将这两个目标文件链接在一起,生成 rect.so(在 Linux 上)或 rect.pyd(在 Windows 上),然后可以使用 import rect 在 Python 中导入(如果忘记链接 Rectangle.o,在 Python 中导入库时会遇到缺少符号的错误)。

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

Cython 版本 0.21 之前的 cythonize() 函数不识别 language 选项,需要将其指定为描述扩展的 Extension 的选项,然后由 cythonize() 处理,如下所示。

from setuptools import Extension, setup
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 setuptools 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

注意

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

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

cpp_locals 指令

编译指令 cpp_locals 是一个实验性功能,它使 C++ 变量的行为类似于普通的 Python 对象变量。使用此指令,它们只在第一次赋值时初始化,因此不再需要无参构造函数来进行栈分配。尝试访问未初始化的 C++ 变量将生成一个 UnboundLocalError(或类似错误),就像 Python 变量一样。例如

def function(dont_write):
    cdef SomeCppClass c  # not initialized
    if dont_write:
        return c.some_cpp_function()  # UnboundLocalError
    else:
        c = SomeCppClass(...)  # initialized
        return c.some_cpp_function()  # OK

此外,该指令避免在赋值之前初始化临时 C++ 对象,适用于 Cython 需要在其自身代码生成中使用此类对象的情况(通常用于可能抛出异常的函数的返回值)。

为了提高速度,initializedcheck 指令禁用了对未绑定局部变量的检查。启用此指令后,访问未初始化的变量将触发未定义的行为,完全由用户负责避免此类访问。

目前,cpp_locals 指令使用 std::optional 实现,因此需要 C++17 兼容的编译器。定义 CYTHON_USE_BOOST_OPTIONAL(作为 C++ 编译器的定义)将使用 boost::optional 代替(但更具实验性,尚未经过测试)。该指令可能会带来内存和性能成本,因为需要存储和检查一个布尔值来跟踪变量是否已初始化,但 C++ 编译器应该能够在大多数情况下消除该检查。

注意事项和限制

访问仅限 C 的函数

在生成 C++ 代码时,Cython 会生成函数的声明和调用,假设这些函数是 C++(即,未声明为 extern "C" {...})。如果 C 函数具有 C++ 入口点,则这没问题,但如果它们是仅限 C 的,则会遇到障碍。如果您有一个需要调用纯 C 函数的 C++ Cython 模块,您需要编写一个小型 C++ shim 模块,该模块

  • 在 extern “C” 块中包含所需的 C 头文件

  • 包含最小的 C++ 转发函数,每个函数都调用相应的纯 C 函数

C++ 左值

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