使用现有 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<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
我看到了许多可能的解决方法,但都不是很令人满意:
我可以初始化一个空的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_ptr
,get()
存储的指针和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 结构转换为 pythons 对象会增加引用计数
在 cython 中将 C++ 对象转换为 python 对象?