Skip to content

在 Cython 中使用 C ++

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

概述

Cython 自带对大多数 C ++语言的支持。具体来说:

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

流程概述

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

  • (声明语言)在 setup.py 脚本或在本地源文件中指定 C ++语言,。
  • (提供定义)使用 cdef extern from 代码块和(如果存在)C ++命名空间名称创建一个或多个 .pxd 文件。在这些块中:
    • 将类声明为 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 {

    // 默认构造器
    Rectangle::Rectangle () {}

    // 重载构造器
    Rectangle::Rectangle (int x0, int y0, int x1, int y1) {
        this->x0 = x0;
        this->y0 = y0;
        this->x1 = x1;
        this->y1 = y1;
    }

    // 析构器
    Rectangle::~Rectangle () {}

    // 返回矩形的面积
    int Rectangle::getArea () {
        return (this->x1 - this->x0) * (this->y1 - this->y0);
    }

    // 获取矩形面积
    // 把面积数值放到指针参数中
    void Rectangle::getSize (int *width, int *height) {
        (*width) = x1 - x0;
        (*height) = y1 - y0;
    }

    // 移动矩阵,移动向量为(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 的代码块重 - 只需从 Rectangle.h 复制类名并调整 Cython 语法,现在它变为:

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

添加公共属性

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

cdef extern from "Rectangle.cpp":
    pass

# 用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 异常(见下文)。如果没有此声明,Cython 将不会处理源自构造函数的 C ++异常。

我们使用这几行代码:

cdef extern from "Rectangle.cpp":
    pass

来 Include Rectangle.cpp的 C ++代码。也可以在setuptools中将 Rectangle.cpp 指定为源文件 -- 您可以在 .pyx(不是 .pxd )文件的顶部添加一行编译器指令:

# distutils: sources = Rectangle.cpp

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

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

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

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

# distutils: language = c++

from Rectangle cimport Rectangle

def main():
    rec_ptr = new Rectangle(1, 2, 3, 4)  # 在堆上初始化一个Rectangle对象
    try:
        rec_area = rec_ptr.getArea()
    finally:
        del rec_ptr  # 删除在堆上分配的Rectangle对象

    cdef Rectangle rec_stack  # 在栈上初始化一个Rectangle对象

这一行代码:

# 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 __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)

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

# distutils: language = c++

from Rectangle cimport Rectangle

cdef class PyRectangle:
    cdef Rectangle c_rect

    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)

    # 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 ++属性被赋值的时候才初始化它们。一个方便且安全地执行手动分配和解除分配的地方是 cinitdealloc 方法中 -- 这两个方法Cython保证会在创建和删除 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, int x0, int y0, int x1, int y1):
        self.c_rect = new Rectangle(x0, y0, x1, y1)

    def __dealloc__(self):
        del self.c_rect

编译和导入

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

from distutils.core 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 ++对象,则必须进行解除引用以避免执行指针算术而不是对对象本身进行算术运算:(译者注:Cython语义仅支持 ptr[0] 这种解除引用的方式,不支持 *ptr)

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 被声明为类型 vector<int>::iterator

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

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

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

  • 用于解除引用运算符的 cython.operator.dereferencedereference(foo) 将生成 C ++代码 *(foo)
  • 用于预增运算符的 cython.operator.preincrementpreincrement(foo) 将生成 C ++代码 ++(foo) 。类似地还有 predecrement (预减)、postincrement (后增)和 postdecrement (后减)
  • 用于逗号运算符的 cython.operator.commacomma(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++

# 引入解除引用运算符和预增运算符
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 内置类型之间双向强制转换。转换是通过赋值给类型变量(包括类型化函数参数)或通过显式强制转换来触发的,例如:

# distutils: language = c++

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'

下面的强制转换是可以的。

Python type => C ++类型 => Python 类型
bytes 字节类 std :: string bytes
iterable 可迭代对象 std :: vector list
iterable std :: list list
iterable std :: set tuple 元祖
iterable std :: unordered_set set
iterable std :: map dict
iterable std :: unordered_map dict
iterable (长度为2) std :: pair tuple(长度为2)
complex std :: complex complex

所有转换都会创建一个新容器并将数据复制到其中。容器中的内容自动转换成相应的类型,包括递归地转换容器内的容器。例如,C ++ vector of maps of strings

请意识到这些转换都会有一些潜在的问题,可以在 如何定位问题 这里查看。(译者注:该文档还没有翻译完成)

支持通过 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() 赋值的类型来进行类型推理。(译者注:查看编译器指令这一节的 infer_type 指令)。

注意: 支持切片 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 异常(译者注:通过添加 except + )。例如,

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
(所有其他情况) 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

如果一些函数内的问题引发了 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逐字地将C代码添加到pxd文件中的特性,并创建了一个新的Python异常类型 CustomLogicError 并使用自定义的异常处理器将其映射到 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会处理的异常的逻辑。关于标准异常的处理器代码可以在这里看。

还有一种特殊的形式用于声明那些 可能引发 Python 或者 C ++异常 的函数:

cdef int raise_py_or_cpp() except +*

静态成员方法

如果 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& 语法声明左值引用。但请注意,没有必要将 extern 函数的参数声明为引用(const 或其他),因为它对调用者的语法没有影响。

枚举类

在C ++模式下,Cython支持枚举类(或者作用域内的枚举):

cdef enum class Cheese:
    cheddar = 1
    camembert = 2

和"普通的"枚举类型一样,你可以以类型属性的方式访问这些枚举量。和普通枚举类型不同的是,这些枚举量对与 Enclosing 作用域不可见(译者注:这也正是C ++11 引入枚举类的原因。Enclosing是一个Python中存在的变量作用域范围,常见于闭包的概念中,没找到合适的词汇)

cdef Cheese c1 = Cheese.cheddar  # 可以
cdef Cheese c2 = cheddar  # 会报错

当然,你也可以声明枚举类的潜在类型。这对于用潜在类型来声明外部的枚举类而言很重要。

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

cpdef 来创建枚举类将会创造一个 PEP-435风格 的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 异常。

另一个与仅 C ++ 11 的 RTTI 相关类std::type_indexlibcpp.typeindex 的方式提供。

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

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

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.cppRectangle 类的实现)并将两个目标文件一起链接到 (Linux 上的) rect.so 或 (在 Windows 上) rect.pyd ,然后可以使用 import rect 在 Python 中导入(如果忘记链接 Rectangle.o ,则在 Python 中导入库时将丢失标识符)。

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

0.21及之前版本的Cython 的 cythonize() 功能无法识别 language 选项,需要将其指定为描述扩展名的 Extension 选项,然后由 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

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

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

cpp_locals指令

cpp_locals 这个编译器指令是实验性的,用来让C ++对象表现地更像是Python对象变量。在启用这个指令的时候,C ++变量仅在第一次赋值的时候被初始化,因此不再需求一个无参构造器来在堆栈上分配。尝试访问一个没有初始化的 C ++变量会产生 UnboundLocalError (或者类似的),就像Python对象那样。比如:

def function(dont_write):
    cdef SomeCppClass c  # 没有初始化
    if dont_write:
        return c.some_cpp_function()  # UnboundLocalError
    else:
        c = SomeCppClass(...)  # 初始化了
        return c.some_cpp_function()  # OK

不仅如此,这个指令还能够,在Cython需要在生成代码中使用临时C ++对象时(通常是为了存储可能抛出异常的函数返回值),避免在这些C ++临时对象被赋值前初始化,

为了追求更快的速度, initializedcheck 这个编译器指令可以禁止执行对未绑定的局部变量进行检测。在该指令被开启时,访问一个没有被初始化的变量会引发无法预料的行为(译者注:直接访问可能会抛出 UnboundError),因此使用者需要自己避免这种情况的发生。

cpp_locals 这个编译器指令目前是用 std::optional 来实现的,因此需要兼容 C ++17的编译器。 给C ++编译器定义 CYTHON_USE_BOOST_OPTIONAL的话则会用 boost::optional 来代替(译者注:代替默认的 std::optional 实现,但是更加实验性、未经测试)。这条编译器指令可能会带来内存和性能的额外开销,因为需要存储、检查一个追踪变量是否已经初始化的布尔型变量,但是C ++编译器应该能在大多数场景下消除这个检查。

警告和限制

访问仅限 C 的函数

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

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

C ++左值

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



回到顶部