pythonc++implicit-conversionpybind11

How to bind implicit constructor/conversion in Python with pybind11?


I'm trying to mimic implicit conversion/construction of a simple struct in Python using pybind11:

struct MyType {
    double a;
    double b;
};

void func(size_t foo, const MyType& bar) {
    // ...
}

// ...


PYBIND11_MODULE(pymylib, module) {
    // ...
    py::class_< MyType >(module, "MyType")
        .def(py::init< double, double >(), "a"_a, "b"_a)
        .def(py::init([](const std::array< double, 2 >& ab){ return MyType({ab[0], ab[1]}); }), "ab"_a = std::array< double, 2>{ 1.0, 0.25 })   // not implicit
        .def_readwrite("a", &MyType::a)
        .def_readwrite("b", &MyType::b);

    py::implicitly_convertible< std::array< double, 2 >, MyType >();   // conversion is not implicit

    module.def("func", &func, "foo"_a, "bar"_a);
}

While in C++ one can use brace initialization {} without having to implicitly call MyType constructor (simplifying the syntax), I am still unable to have an implicit conversion from a list object when passing MyType as argument to func:

obj = module.MyType(8.7, 5.6)

module.func(47, obj)                        # works
module.func(47, module.MyType([4.1, 7.8]))  # works
module.func(47, [4.1, 7.8])                 # does not work ('incompatible function arguments')

From what I understand from pybind11 documentation, the constructor declaration py::init< double, double >() should bind brace-initialization implicitly, but it's not working as I expected:

TypeError: func(): incompatible function arguments. The following argument types are supported:
    1. module.func(a: float, b: module.MyType)

I tried adding a custom constructor accepting a list/array, but it is still not called implicitly.

Due to the simplicity of the struct, having to explicitly call the constructor seems unnecessary, particularly if defined in a Python submodule or nested on another class; How - if possible - do I achieve this behavior in Python?


Solution

  • The documentation you are citing refers to the way the python initializer calls the C++ constructor. It makes no remark as far as I can tell regarding implicit conversion from list. As you suspected, you need to register a special py::init() for it.

    Now regarding why your special py::init() does not work: My best guess is you are missing #include <pybind11/stl.h>, see https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html:

    When including the additional header file pybind11/stl.h, conversions between std::vector<>/std::deque<>/std::list<>/std::array<>/std::valarray<>, std::set<>/std::unordered_set<>, and std::map<>/std::unordered_map<> and the Python list, set and dict data structures are automatically enabled. The types std::pair<> and std::tuple<> are already supported out of the box with just the core pybind11/pybind11.h header.

    Here is a working demo: https://godbolt.org/z/WrPrb8cM8