使用现有 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++ 的共享指针更熟悉。
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
)。
请注意,创建新的 python 对象时会产生一些开销,即使它们确实指向相同的 C++ 结构。不多,但还是。
IMO C/C++ 指针的这种自动取消分配和引用计数是 Python 的 C 扩展 API 的最大特性之一。由于所有作用于 Python 对象(单独),C/C++ 结构需要包装在兼容的 Python object
class 定义中。
注意 - 我的经验主要是在 C 中,以上可能需要调整,因为我对常规 C 指针比 C++ 的共享指针更熟悉。