Jul*_*ult 6 python arrays api-design numpy pybind11
使用pybind11 ,如何使用NumPy公开 POD 结构数组,同时让它们向用户显示为漂亮的 Python 对象?
我使用pybind11向Python公开C 风格 API。有一些类型在 C 中实现为简单的 POD 结构,在 Python 中作为不透明对象更有意义。pybind11允许我做到这一点并定义对象在 Python 中的样子。
我还想公开一个动态分配的数组。使用pybind11和NumPy这样做 是可能的,但我还没有找到一种与我已经公开类型本身的方式兼容的方法。
我最终得到了两种不同的 Python 类型,尽管底层的 C 类型是相同的,但它们彼此不兼容。
我正在寻找一种不涉及不必要副本的解决方案。由于所有数据都是 POD,我认为应该可以将数据重新解释为 C 端的结构或 Python 端的不透明对象。
C API 是固定的,但我可以自由地设计 Python API。
在 C/C++ 方面,类型如下所示:
struct apiprefix_opaque_type
{
int inner_value;
};
Run Code Online (Sandbox Code Playgroud)
使用pybind11,我将结构公开为不透明对象。不暴露并不重要inner_value,但它对用户来说根本没有太大价值,拥有更高级别的类型更有意义。
namespace py = pybind11;
void bindings(py::module_& m)
{
py::class_<apiprefix_opaque_type>(m, "opaque_type")
.def(py::init([]() {
apiprefix_opaque_type x;
x.inner_value = -1;
return x;
}))
.def("is_set", [](const apiprefix_opaque_type& x) -> bool { return x.inner_value != -1; });
m.def("create_some_opaque", []() -> apiprefix_opaque_type {
apiprefix_opaque_type x;
x.inner_value = 42;
return x;
});
}
Run Code Online (Sandbox Code Playgroud)
完成此操作后,在 Python 端我就拥有了我想要的 API 行为。
>>> a = apitest.opaque_type()
>>> a.inner_value # Demonstrating that inner_value is not exposed.
AttributeError: 'apitest.opaque_type' object has no attribute 'inner_value'
>>> a.is_set()
False
>>> b = apitest.create_some_opaque()
>>> b.is_set()
True
Run Code Online (Sandbox Code Playgroud)
在 API 的其他地方,我有一个包含这些数组的结构,作为指针和计数对。为了简单起见,我们假设它是一个全局变量(即使实际上,它是另一个动态分配对象的成员)。
struct apiprefix_state
{
apiprefix_opaque_type* things;
int num_things;
};
apiprefix_state g_state = { nullptr, 0 };
Run Code Online (Sandbox Code Playgroud)
这个数组足够大,我关心性能。因此我限制避免不必要的复制。
我希望能够从 Python 读取数组、修改数组或完全替换数组。我认为如果最后设置数组的人保留对其的所有权,则更有意义,但我不完全确定。
这是我当前使用NumPy公开数组的尝试。
void more_bindings(py::module_& m)
{
py::class_<apiprefix_state>(m, "state")
.def(py::init([]() {
return g_state;
}))
.def("create_things",
[](apiprefix_state&, int size) -> py::array {
auto arr = py::array_t<apiprefix_opaque_type>(size);
return std::move(arr);
})
.def_property(
"things",
[](apiprefix_state& state) {
auto base = py::array_t<apiprefix_opaque_type>();
return py::array_t<apiprefix_opaque_type>(state.num_things, state.things, base);
},
[](apiprefix_state& state, py::array_t<apiprefix_opaque_type> things) {
state.things = nullptr;
state.num_things = 0;
if (things.size() > 0)
{
state.num_things = things.size();
state.things = (apiprefix_opaque_type*)things.request().ptr;
}
});
}
Run Code Online (Sandbox Code Playgroud)
鉴于我对 Python 内存管理的初步了解,我强烈怀疑所有权没有正确实现。
但这个问题所涉及的问题是 NumPy 不明白什么apiprefix_opaque_type是。
>>> state = apitest.state()
>>> state.things
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: NumPy type info missing for struct apiprefix_opaque_type
>>>
Run Code Online (Sandbox Code Playgroud)
如果我添加一个 dtype 声明...
PYBIND11_NUMPY_DTYPE(apiprefix_opaque_type, inner_value);
Run Code Online (Sandbox Code Playgroud)
...现在 NumPy 可以理解它,但现在有两种不兼容的 Python 类型引用相同的 C 类型。此外,还公开了实现细节inner_value。
>>> state = apitest.state()
>>> state.things
array([], dtype=[('inner_value', '<i4')])
>>> state.things = state.create_things(10)
>>> a = apitest.opaque_type()
>>> a
<apitest.opaque_type object at 0x000001BABE6E72B0>
>>> state.things[0] = a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'apitest.opaque_type'
>>>
Run Code Online (Sandbox Code Playgroud)
如何公开我的不透明对象数组?
如果你只是想暴露一系列的东西那么你可以这样做
apiprefix_opaque_type& apiprefix_state_get(apiprefix_state& s, size_t j)
{
return s.things[j];
}
void apiprefix_state_set(apiprefix_state& s, size_t j, const apiprefix_opaque_type& o)
{
s.things[j] = o;
}
py::class_<apiprefix_state>(m, "state")
// ...
.def("__getitem__", &apiprefix_state_get)
.def("__setitem__", &apiprefix_state_set)
Run Code Online (Sandbox Code Playgroud)
添加范围检查显然是一个好主意。(你可以使用 lambda,我只是发现显式函数更具可读性)。
当您包装things在 numpy 数组中时,您将其公开为buffer,而结构化数据类型仅提供有关哪些偏移量处的哪些字节应解释为ints 的信息。因此,您实际上可以state.things[0] = 42在上面编写(更一般地,对于具有多个成员的结构,您可以分配一个元组)。但它不知道如何提取intfromapiprefix_opaque_type并将其分配给 dtype 定义的字段。
如果您想公开things为 numpy 数组,那么正如您所指出的,所有权是一个重要的问题。如上所述,Python 将拥有create_things底层内存创建的所有数组并对其进行管理。但是,您的设置器存在一些问题。第一的
state.things = nullptr;
state.num_things = 0;
Run Code Online (Sandbox Code Playgroud)
如果 指向的内存state.things不是由 python 管理的,则可能存在内存泄漏。其次在这一行
state.things = (apiprefix_opaque_type*)things.request().ptr;
Run Code Online (Sandbox Code Playgroud)
您正在引用由 python 管理的内存,而没有引用计数,因此有可能会apiprefix_state留下things指向 python 已垃圾收集的内存。
看来您可能想要公开g_state可能由 C++ 管理的全局。在这种情况下,一种可能的方法是
pybind11::capsule nogc(values, [](void *f) {});
return pybind11::array_t<apiprefix_opaque_type>(
{ g_state.num_things },
{ sizeof(apiprefix_opaque_type) },
g_state.things,
nogc
);
Run Code Online (Sandbox Code Playgroud)
或者,您可以直接使用缓冲区协议或内存视图。
如果您确实希望始终引用全局状态,那么从初始值设定项返回它是不寻常的
.def(py::init([]() { return g_state; }))
Run Code Online (Sandbox Code Playgroud)
通常它会是这样的
.def_static("get_instance", ... )
Run Code Online (Sandbox Code Playgroud)
但请注意,这并不能完全满足您的要求,因为它会复制g_state。