使用现有 C 对象初始化 Cython 对象

Initializing Cython objects with existing C Objects

C++ 模型

假设我有以下 C++ 数据结构,我希望公开给 Python。

#include <memory>
#include <vector>

struct mystruct
{
    int a, b, c, d, e, f, g, h, i, j, k, l, m;
};

typedef std::vector<std::shared_ptr<mystruct>> mystruct_list;

提升Python

我可以使用 boost::python 和下面的代码相当有效地包装这些,很容易让我使用现有的 mystruct(复制 shared_ptr)而不是重新创建现有的对象。

#include "mystruct.h"
#include <boost/python.hpp>

using namespace boost::python;


BOOST_PYTHON_MODULE(example)
{
    class_<mystruct, std::shared_ptr<mystruct>>("MyStruct", init<>())
        .def_readwrite("a", &mystruct::a);
        // add the rest of the member variables

    class_<mystruct_list>("MyStructList", init<>())
        .def("at", &mystruct_list::at, return_value_policy<copy_const_reference>());
        // add the rest of the member functions
}

Cython

在 Cython 中,我不知道如何在不复制基础数据的情况下从 mystruct_list 中提取项目。我不知道如何从现有的 shared_ptr<mystruct> 初始化 MyStruct,而无需以各种形式之一复制所有数据。

from libcpp.memory cimport shared_ptr
from cython.operator cimport dereference


cdef extern from "mystruct.h" nogil:
    cdef cppclass mystruct:
        int a, b, c, d, e, f, g, h, i, j, k, l, m

    ctypedef vector[v] mystruct_list


cdef class MyStruct:
    cdef shared_ptr[mystruct] ptr

    def __cinit__(MyStruct self):
        self.ptr.reset(new mystruct)

    property a:
        def __get__(MyStruct self):
            return dereference(self.ptr).a

        def __set__(MyStruct self, int value):
            dereference(self.ptr).a = value


cdef class MyStructList:
    cdef mystruct_list c
    cdef mystruct_list.iterator it

    def __cinit__(MyStructList self):
        pass

    def __getitem__(MyStructList self, int index):
        # How do return MyStruct without copying the underlying `mystruct` 
        pass

我看到了很多可能的解决方法,其中 none 个非常令人满意:

我可以初始化一个空的 MyStruct,并在 Cython 中分配给 shared_ptr。但是,这将导致毫无理由地浪费初始化结构。

MyStruct value
value.ptr = self.c.at(index)
return value

我还可以将数据从现有的 mystruct 复制到新的 mystruct。然而,这也有类似的膨胀问题。

MyStruct value
dereference(value.ptr).a = dereference(self.c.at(index)).a
return value

我还可以为每个 __cinit__ 方法公开一个 init=True 标志,如果 C 对象已经存在(当 init 为 False 时),这将阻止在内部重建对象。但是,这可能会导致 灾难性的 问题,因为它会暴露给 Python API 并允许取消引用空指针或未初始化的指针。

def __cinit__(MyStruct self, bint init=True):
    if init:
        self.ptr.reset(new mystruct)

我也可以用 Python 暴露的构造函数重载 __init__(这会重置 self.ptr),但是如果使用 __new__,这会有内存安全风险来自 Python 层。

底线

我喜欢使用 Cython,因为编译速度、语法糖和许多其他原因,而不是相当笨重的 boost::python。我现在正在看 pybind11,它可能会解决编译速度问题,但我还是更喜欢使用 Cython。

有什么方法可以在 Cython 中以惯用的方式完成如此简单的任务?谢谢。

这在 Cython 中的工作方式是让工厂 class 从共享指针创建 Python 对象。这使您无需复制即可访问基础 C/C++ 结构。

示例 Cython 代码:

<..>

cdef class MyStruct:
    cdef shared_ptr[mystruct] ptr

    def __cinit__(self):
        # Do not create new ref here, we will
        # pass one in from Cython code
        self.ptr = NULL

    def __dealloc__(self):
        # Do de-allocation here, important!
        if self.ptr is not NULL:
            <de-alloc>

    <rest per MyStruct code above>

cdef object PyStruct(shared_ptr[mystruct] MyStruct_ptr):
    """Python object factory class taking Cpp mystruct pointer
    as argument
    """
    # Create new MyStruct object. This does not create
    # new structure but does allocate a null pointer
    cdef MyStruct _mystruct = MyStruct()
    # Set pointer of cdef class to existing struct ptr
    _mystruct.ptr = MyStruct_ptr
    # Return the wrapped MyStruct object with MyStruct_ptr
    return _mystruct

def make_structure():
    """Function to create new Cpp mystruct and return
    python object representation of it
    """
    cdef MyStruct mypystruct = PyStruct(new mystruct)
    return mypystruct

请注意 PyStruct 的参数类型是指向 Cpp 结构的指针

mypystruct 是 class MyStruct 的 python 对象,由工厂 class 编辑 return,指的是这 Cpp mystruct 没有复制。根据 make_structure 代码,mypystruct 可以在 def cython 函数中安全地 returned 并在 python space 中使用。

到 return 现有 Cpp mystruct 指针的 Python 对象只需用 PyStruct 包裹它,就像

return PyStruct(my_cpp_struct_ptr)

Cython 代码中的任何位置。

显然只有 def 函数在那里可见,因此如果要在 Python space 中使用 Cpp 函数调用,也需要将它们包装在 MyStruct 中,至少如果您希望 Cython 中的 Cpp 函数调用 class 放弃 GiL(出于显而易见的原因,可能值得这样做)。

有关真实示例,请参阅此 Cython extension code and the underlying C code bindings in Cython. Also see this code for Python function wrapping of C function calls that let go of GIL。不是 Cpp,但同样适用。

另请参阅 official Cython documentation on when a factory class/function is needed (Note that all constructor arguments will be passed as Python objects)。对于内置类型,Cython 会为您进行这种转换,但对于自定义结构或对象,需要工厂 class/function。

Cpp 结构初始化可以在 PyStruct__new__ 中处理,如果需要,根据上面的建议,如果您希望工厂 class 实际为您创建 C++ 结构(真的取决于用例)。

带有指针参数的工厂 class 的好处是它允许您使用 C/C++ 结构的现有指针并将它们包装在 Python 扩展中 class,而不是总是必须创建新的。例如,让多个 Python 对象引用相同的底层 C 结构是完全安全的。 Python 的引用计数确保它们不会过早地取消分配。尽管共享指针可能已经被显式取消分配(例如,通过 del)。

,但您仍应在取消分配时检查 null

请注意,创建新的 python 对象时会产生一些开销,即使它们确实指向相同的 C++ 结构。不多,但还是。

IMO C/C++ 指针的这种自动取消分配和引用计数是 Python 的 C 扩展 API 的最大特性之一。由于所有作用于 Python 对象(单独),C/C++ 结构需要包装在兼容的 Python object class 定义中。

注意 - 我的经验主要是在 C 中,以上可能需要调整,因为我对常规 C 指针比 C++ 的共享指针更熟悉。