Replies: 3 comments
-
@hzhangxyz very interesting concept. I have prepared a little breakdown on my thoughts on that, as I encountered such problem in the past. Let's start from the "core" of language itself. I will present this example: from typing import TypeVar, Generic
T = TypeVar('T', int, float)
class MyNumber(Generic[T]):
def __init__(self, val: T) -> None:
self.val: T = val
def replace(self, val: T) -> None:
self.val = val
def is_negative(self) -> bool:
return self.val < 0
super_int = MyNumber[int](6)
super_int.is_negative()
super_int.replace([0, 1, 2]) # error in mypy but not in the runtime
next_one = MyNumber[dict]({"a": 5}) # error in mypy but not in the runtime
next_one.is_negative() # error in the runtime! Here you may see that in fact you can use types in brackets! You can refer to Python docs on Generics for more info on that. However there is one more important thing, on that same page you may find:
That is why comments With that background you may see what is the main problem when it comes to templated classes. class MyNumber():
def __new__(self, val):
if isinstance(val, int):
return MyNumberInt(val)
# ... ... but this is simply a hacking solution. None of the returned object will be an original class which potential users are expecting to have. Maybe combining this idea with a composition and creating something like: class MyNumber():
def __init__(self, val):
if isinstance(val, int):
self.my_number = MyNumberInt(val)
# ...
def any_method(n):
self.my_number.any_method(n) is a better way to keep the same class name if API for both classes are the same and only type is differencing them. This is the most you can squeeze from Python, and keep it somewhat readable, if you ask me. However, you still need to register multiple classes in your pybind code type by type -- C++ style. Ok, but how it comes that types errors are shown when overloads do not exists? Internal pybind magic: pybind11/include/pybind11/pybind11.h Line 657 in 9aa676d So in theory it could be possible to use some "internal machinery" to force classes initalizers to utilise syntax of Python's typing generics and only then allow the user to create such class. On the other hand it feels very unintuitive from Python's perspective to declare any type and create templated/overloaded solutions and even more... replacing the meaning of type hints to satisfy C++ bindings. It seems like most of problems with the similar nature comes from the fundamental differences between two languages, and there is no easy way to solve them. I would like to hear some more from pybind devs on that topic! |
Beta Was this translation helpful? Give feedback.
-
@jiwaszki Can we use something like this to implement template like python class? class Template(type):
def __new__(meta, name, bases, dict):
dict["specialization_pool"] = {}
dict["__class_getitem__"] = meta._class_getitem
return type(name, bases, dict)
def __class_getitem__(meta, params):
if not isinstance(params, tuple):
params = (params,)
supertypes = tuple(object if isinstance(param, str) else param.stop for param in params)
params = tuple(param if isinstance(param, str) else param.start for param in params)
def _class_getitem(cls, args):
if not isinstance(args, tuple):
args = (args,)
assert len(params) == len(args)
for arg, supertype in zip(args, supertypes):
assert issubclass(arg, supertype)
if args not in cls.specialization_pool:
subtype = type(
f"{cls.__name__}[{','.join(arg.__name__ for arg in args)}]",
(cls,),
{param: arg for param, arg in zip(params, args)},
)
cls.specialization_pool[args] = subtype
return cls.specialization_pool[args]
return type(f"Template[{','.join(params)}]", (meta,), {"_class_getitem": _class_getitem})
class MyNumber(metaclass=Template["T":int | float]):
def __init__(self, val):
assert isinstance(val, self.T)
self.val = val
def replace(self, val):
assert isinstance(val, self.T)
self.val = val
def is_negative(self):
return self.val < 0
super_int = MyNumber[int](6)
super_int.is_negative()
# super_int.replace([0, 1, 2]) # error in the runtime
# next_one = MyNumber[dict]({"a": 5}) # error in the runtime should the specification |
Beta Was this translation helpful? Give feedback.
-
@hzhangxyz this could be one way to solve your issue. However, if you want some pybind based solution, I came up with a little "proof of concept". Your input gave me some great ideas! Note: I tried to rely on pybind itself, without any help of low-level Python C API. Let's start with header file. Here we are preparing base for our bindings. That includes proxy classes and applying inheritance that will be very useful later on. Additional inheritance is providing us with methods from base class that will be uniform between specialisations, as well as playing a role of initial "constructor". Please notice two helper functions called #include <string>
#include <iostream>
#include <pybind11/pybind11.h>
namespace py = pybind11;
template <class T>
class MyNumber
{
public:
MyNumber(){};
void show(const T &arg)
{
std::cout << arg << "\n";
}
};
// Proxy base class
class PyNumberBase
{
};
// Proxy class for actual template
template <typename T>
class PyNumber : public MyNumber<T>, public PyNumberBase
{
};
bool check_key(py::object key, py::object obj)
{
return key.is(py::type::of(obj));
}
template <typename T>
py::type get_class_pytype()
{
auto tmp = T();
py::object obj = py::cast(tmp);
return py::type::of(obj);
}
// B - base class, "parent"
// C - templated class
// T - args...
template <typename B, template <typename T> typename C, typename T>
py::class_<C<T>> wrap(py::module m, std::string type_name)
{
py::class_<C<T>, B> cls(m, type_name.c_str());
cls.def(py::init<>());
cls.def("show", [](C<T> &self, const T &arg)
{ self.show(arg); });
return std::move(cls); // return allows us to update class later
} Now we can provide bindings. First things first, wrap base class and name it properly, apply all "common" functions to it. Now #include "code.hpp"
namespace py = pybind11;
PYBIND11_MODULE(mymodule, m)
{
// Need to wrap first!
py::class_<PyNumberBase> cls(m, "MyNumber");
cls.def("__class_getitem__", [](py::object &key)
{
if (check_key(key, py::float_()))
{
return get_class_pytype<PyNumber<float>>();
}
else if (check_key(key, py::int_()))
{
return get_class_pytype<PyNumber<int>>();
}
else
{
throw py::type_error("Type is not supported!");
}
});
cls.def("__repr__", [](PyNumberBase &self) {
return py::str("<class MyNumber>");
});
// MyNumber<float> bindings
auto cls_float = wrap<PyNumberBase, PyNumber, float>(m, "MyFloat");
// Using PyNumberBase as self is valid
// Let's override __repr__ method from base class
cls_float.def("__repr__", [](PyNumberBase &self) {
return py::str("<class MyNumber[float]>");
});
// MyNumber<int> bindings
auto cls_int = wrap<PyNumberBase, PyNumber, int>(m, "MyInt");
// Using PyNumber<...> as self is also valid
// Let's add new function
cls_int.def("is_negative", [](PyNumber<int> &self, int val) {
return val < 0;
});
} Let's see how it looks in the interpreter... and it looks great!
However there is one little thing to work on. As
Maybe I will have some free time soon to build upon that sample... 🤔 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
If I want to wrap a template type with several specialization, I need to wrap them seperately, although it is easy to write a template function to do it all. However, if it is supported by pybind, it would be better.
For example, a c++ template class is:
Currently I need to wrap by the following code
And call wrap with different T one by one.
I hope we can write code like this:
In fact, CxxWrap.jl (similar things to pybind11 for julia lang) is using this way. https://github.com/JuliaInterop/CxxWrap.jl#template-parametric-types
What else:
Maybe in the feature, we can write code in python like this:
However, it seems currently python constructor cannot obtains the type argument in the square bracket.
Beta Was this translation helpful? Give feedback.
All reactions