为什么 Python 在调用派生自 C++ std::vector 的 pybind11 type_cast-ed class 方法时会报错?

Why Python complains when called method of pybind11 type_cast-ed class that derives from C++ std::vector?

使用 pybind11 我包装了一个我无法修改的 C++ 库。问题来自 std::vector 派生的 class。 (注:这是我的第一个 pybind11 项目。我不是 Python 中的 'fluent'。我在网上寻找解决方案但没有成功。)

简介。 E 的实例携带错误数据。 E 只能由 Es 实例化 - 一个收集器。 Es 通过 addFront(...) 等方法用 E 的新实例扩大,同时从失败的方法返回(即展开调用堆栈)。

极简源代码:

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace py = pybind11;
using namespace pybind11::literals;

// Classes
enum class ID { unknown, wrongParam };

class E {
public:
    ID GetID() const { return id; }

protected:
    E( ID _id ) { id = _id; };
    ID  id;
    friend class Es;
};

class Es : public std::vector< E > {
public:
    Es() {}
    Es( ID _id ) { push_back( E( _id ) ); }

    Es& addFront( ID _id ) {
        insert( begin(), E( _id ) );    // Base class methods!
        return *this;
    }
};

由于从 std::vector 派生,据我了解,应该应用 Estype_caster 所以它可以用作 Python 侧的 list:

namespace pybind11 { namespace detail {
    template <> struct type_caster< Es > : list_caster< Es, E > {};
}} // namespace pybind11::detail

pybind11部分是:

void Bind( py::module_& m ) {
    py::enum_< ID >( m, "ID" )
        .value( "unknown", ID::unknown )
        .value( "wrongParam", ID::wrongParam );

    py::class_< E >( m, "E" )
        .def( "GetID", &E::GetID );

    py::class_< Es >( m, "Es" )
        .def( py::init<>() )
        .def( py::init< ID >(), "id"_a )
        .def( "addFront", &Es::addFront );
}

当执行此 Python 代码时:

from AWB import ID, E, Es
es = Es( ID.wrongParam )
es.addFront( ID.unknown )

python抱怨:

E:\Projects\AWB>py
Python 3.8.10 (tags/v3.8.10:3d8993a, May  3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from AWB import ID, E Es
>>> es = Es( ID.wrongParam )
>>> es.addFront( ID.unknown )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: addFront(): incompatible function arguments. The following argument types are supported:
    1. (self: List[AWB.E], arg0: AWB.ID) -> List[AWB.E]

Invoked with: <AWB.Es object at 0x000000000260CEB0>, <ID.unknown: 0>
>>>

问:我做错了什么?

问:为什么 arg0: AWB.IDID.unknown 不兼容?

问:也许 类型转换 应该更精确?

好吧,在现实世界中,我不希望 Es 会在 Pyhon 端扩大。大多数情况下,我需要导出集合(以人类可读的方式)Es 到目前为止(在 C++ 端)收集的内容。但由于我正在编写测试用例 - 我需要确保它有效。

问:即使有可能,在 Python 端使用 addFront 将项目添加到 C++ std::vector... 是否会自动 在 Python 的 list?

中可见

既然您想使用特定于 Es 的 C++ 成员函数,我认为您不应该在这种情况下尝试使用类型转换。如果你要类型转换 Es,这意味着你的 Python 类型将是 E 对象的复制列表,但它不会有像 addFront 这样的方法 - 你会有append

您可以做的是包装您的类型 as an opaque type,并导出您需要的方法。此示例来自 pybind11 文档:

py::class_<std::vector<int>>(m, "IntVector")
    .def(py::init<>())
    .def("clear", &std::vector<int>::clear)
    .def("pop_back", &std::vector<int>::pop_back)
    .def("__len__", [](const std::vector<int> &v) { return v.size(); })
    .def("__iter__", [](std::vector<int> &v) {
       return py::make_iterator(v.begin(), v.end());
    }, py::keep_alive<0, 1>()) /* Keep vector alive while iterator is used */
    // ....

根据,我稍微修改了代码。以下是可能对其他人有帮助的更改。

首先,将 type_caster 代码块替换为:

PYBIND11_MAKE_OPAQUE(Es);

如果 py::class_<Es>(m, "Es") 的绑定说明被放大为:

    .def("__len__", [](const Es& v) {return v.size();})

Python 序列执行为:

>>> from AWB import ID, E, Es
>>> es = Es(ID.wrongParam)
>>> es = es.addFront(ID.unknown)
>>> len(es)
2