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

Ale*_*agh 8 c++ python cython boost-python

C++模型

假设我有以下要向Python公开的C++数据结构.

#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;
Run Code Online (Sandbox Code Playgroud)

提升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
}
Run Code Online (Sandbox Code Playgroud)

用Cython

在Cython中,我不知道如何从mystruct_list中提取项目,而不复制底层数据.我不知道如何MyStruct从现有的初始化shared_ptr<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
Run Code Online (Sandbox Code Playgroud)

我看到许多可能的解决方法,但没有一个是非常令人满意的:

我可以初始化一个空MyStruct,并在Cython中分配shared_ptr.然而,这将导致浪费一个初始化的结构,绝对没有理由.

MyStruct value
value.ptr = self.c.at(index)
return value
Run Code Online (Sandbox Code Playgroud)

我也可以将数据从现有数据复制mystruct到新数据mystruct.然而,这遭受类似的膨胀.

MyStruct value
dereference(value.ptr).a = dereference(self.c.at(index)).a
return value
Run Code Online (Sandbox Code Playgroud)

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

def __cinit__(MyStruct self, bint init=True):
    if init:
        self.ptr.reset(new mystruct)
Run Code Online (Sandbox Code Playgroud)

我也可以__init__使用暴露于Python的构造函数(它会重置self.ptr)重载,但是如果__new__从Python层使用它会带来危险的内存安全性.

底线

我很乐意使用Cython,编译速度,语法糖和许多其他原因,而不是相当笨重的boost :: python.我现在正在看pybind11,它可能会解决编译速度问题,但我仍然希望使用Cython.

有没有什么办法可以在Cython中以惯用方式完成这么简单的任务?谢谢.

dan*_*nny 5

这在Cython中的工作方式是通过一个工厂类从共享指针创建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
Run Code Online (Sandbox Code Playgroud)

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

mypystruct然后是类的python对象,MyStruct由工厂类返回,它引用Cpp mystruct而不复制.mypystruct可以安全地返回defcython函数,并在每个make_structure代码的python空间中使用.

要返回现有的CPP Python对象mystruct指针只是把它包PyStruct

return PyStruct(my_cpp_struct_ptr)

你的Cython代码中的任何地方.

显然只有def函数在那里是可见的,所以如果要在Python空间中使用Cpp函数调用也需要在MyStruct中包装,至少如果你想让Cython类中的Cpp函数调用放开GiL(可能值得这么做的原因).

有关实际示例,请参阅Cython中的Cython扩展代码基础C代码绑定.另请参阅此代码,了解C函数调用的Python函数包装,它们放弃了GIL.不是Cpp,但同样适用.

有关何时需要工厂类/函数,请参阅官方Cython文档(Note that all constructor arguments will be passed as Python objects).对于内置类型,Cython为您进行此转换,但对于自定义结构或对象,需要工厂类/函数.

CPP的结构初始化可以在处理__new__PyStruct,如果需要上面,如果你想在工厂类实际上是创建C++结构为你(取决于所使用的情况下,真的),每建议.

具有指针参数的工厂类的好处是它允许您使用C/C++结构的现有指针并将它们包装在Python扩展类中,而不是总是必须创建新的.例如,有多个Python对象引用相同的底层C结构是完全安全的.Python的引用计数确保它们不会过早地被解除分配.在解除分配时仍应检查null,因为共享指针可能已经明确地取消分配(例如,by del).

但是请注意,创建新的python对象时会有一些开销,即使它们确实指向相同的C++结构.不是很多,但仍然.

IMO对C/C++指针的自动解除分配和引用计数是Python C扩展API的最大特性之一.由于所有这些都作用于Python对象(单独),因此需要将C/C++结构包装在兼容的Python object类定义中.

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