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

Posted

技术标签:

【中文标题】使用现有 C 对象初始化 Cython 对象【英文标题】:Initializing Cython objects with existing C Objects 【发布时间】:2017-06-21 21:30:28 【问题描述】:

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;

提升 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 中,我不知道如何在不复制基础数据的情况下从 mystruct_list 中提取项目。我不知道如何从现有的shared_ptr&lt;mystruct&gt; 初始化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

我看到了许多可能的解决方法,但都不是很令人满意:

我可以初始化一个空的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 并允许取消引用 null 或未初始化的指针。

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

我还可以使用 Python 公开的构造函数重载 __init__(这将重置 self.ptr),但如果从 Python 层使用 __new__,这将带来内存安全风险。

底线

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

有什么方法可以让我在 Cython 中习惯性地完成这样一个简单的任务?谢谢。

【问题讨论】:

return dereference(self.c.at(index).get()) 有效吗? IE。从向量中检索shared_ptrget() 存储的指针和dereference 它。或者可能只是 return dereference(self.c.at(index))(在 C++ 中,您可以直接取消引用共享指针)。 这会给你一个mystruct而不是MyStruct。我猜你需要第二个构造函数def __cinit__(MyStruct self, new_ptr): self.ptr.reset(new_ptr),然后再做return MyStruct(self.c.at(index)) 是的,不幸的是,只有几个问题@HenriMenke。 Cython 不允许我在 def 中使用 C 类型作为参数(与 cdef 不同),并且初始化函数不能仅是 cdef。如果 Cython 让我用 cdef 定义自定义构造函数,那将解决所有问题。不幸的是,事实并非如此。这可能可以通过 Python C-API 或通过重载 __init__ 来实现,但文档非常清楚地指出,当调用 __init__ 时,对象应该是有效的,并且可能根本不会调用 __init__。 cython.readthedocs.io/en/latest/src/userguide/… 重载的__cinit__ 加上return MyStruct.__new__(self.c.at(index)) 可以工作。 »如果在 Python 层使用__new__,这将有风险的内存安全«您正在将您的标准提高到一个不合理和荒谬的水平。如果有人在 Python 级别调用 __new__,他们会更好地知道自己在做什么。如果你想要内存安全,只需用 Python 重写你的整个代码。 【参考方案1】:

这在 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

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

mypystruct then 是类MyStruct 的python 对象,由工厂类返回,它指的是 cpp mystruct 无需复制。根据make_structure 代码,mypystruct 可以在def cython 函数中安全返回并在 python 空间中使用。

要返回现有 Cpp mystruct 指针的 Python 对象,只需将其包装为 PyStruct 就像

return PyStruct(my_cpp_struct_ptr)

在 Cython 代码中的任何位置。

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

有关真实示例,请参阅此 Cython extension code 和 underlying C code bindings in Cython。另见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 会为您执行此转换,但对于自定义结构或对象,则需要工厂类/函数。

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

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

请注意,尽管创建新的 Python 对象确实指向相同的 C++ 结构,但仍存在一些开销。不是很多,但仍然。

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

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

【讨论】:

以上是关于使用现有 C 对象初始化 Cython 对象的主要内容,如果未能解决你的问题,请参考以下文章

随笔--类和对象初阶问题总结(面试)

如何通过cython接口返回对象引用的c ++函数

Cython:将 C 结构转换为 pythons 对象会增加引用计数

在 cython 中将 C++ 对象转换为 python 对象?

如何使用 Cython 向 Python 公开返回 C++ 对象的函数?

Cython 获取 C++ 对象列表的长度