wlav / cppyy

Other
400 stars 41 forks source link

Overriding overloaded member functions selectively? #94

Open torokati44 opened 1 year ago

torokati44 commented 1 year ago

I understand that due to the impedance mismatch caused by the lack of overloading in Python, there is no way to seamlessly override only one overload of a C++ member function in a Python subclass.

So, (I guess) due to the lack of a better option, cppyy simply overrides every overload, and dispatches the calls to any of them to the same (possibly variadic) Python override. And then I can manually decide what to do with the actually received args, and based on them, have a way to manually "un-override" some parameter combinations, by calling back to the method in the superclass manually.

This is mostly fine, but would be nicer if it wouldn't be necessary. Say, if cppyy provided a way for me to annotate (in a @decorator maybe) which overloads I wish my function in the Python subclass to override. Similar to how there is a way to manually select which overload I want to call.

wlav commented 1 year ago

The problem with a decorator approach would be that the decorator is executed after the class and its functions have been created. At that point, the dispatcher has to already exist in C++ land (it's the base class of the Python class, so it needs to exist before the Python one can). Iow., a decorator can only "undo" dispatching already created, but can not "direct" the creation of said dispatcher methods. I expect the same to be true for type annotations (added to the function once it exists, not available yet on class creation).

To "undo" things in Python, with a decorator or otherwise, requires signature mapping, which isn't easy given that e.g. both a float and a double become a Python float, so that seems hard to automate as well.

The dispatcher class is also not accessible from Python to modify after the fact. (This was done to keep the inheritance hierarchy clean-looking in Python-land, e.g. when inspected with help().)

That only leaves an explicit intermediate in C++. Since you don't need to implement the method, that should be easy. A basic example, where only the double overload gets overridden in Python:

import cppyy

cppyy.cppdef("""
class MyClass {
public:
    virtual ~MyClass() {}

    virtual std::string ol(int) { return "int"; }
    virtual std::string ol(double) { return "double"; }
};

std::string callback_i(MyClass& m) {
    return m.ol(1);
}

std::string callback_d(MyClass& m) {
    return m.ol(1.);
}
""")

cppyy.cppdef("""
class MyClassDisp : public MyClass {
public:
    virtual std::string ol(double) = 0;
};
""")

class MyClass(cppyy.gbl.MyClassDisp):
    def ol1(self):
        print("from Python!")

    def ol(self, t):
        return str(type(t))

m = MyClass()

print(cppyy.gbl.callback_i(m))
print(cppyy.gbl.callback_d(m))

Maybe this can be made somewhat automatic using a metaclass, but a decorator would suffer from the same issue. Still, if you have many Python-side derived classes from a single interface that needs to be so restricted, the above approach would cut down on the amount of work involved.

torokati44 commented 1 year ago

Thank you for the idea for a workaround and the detailed example!

Unfortunately I don't think this one will work out for us, for a number of reasons:

However, I believe the concrete cases we have encountered so far will fortunately be made to work by simply requiring the method in the Python subclass to have the maximum number of arguments among the overloads in the base. I will add further details once we come across something that does not work out nicely like that.

wlav commented 1 year ago

If they always differ in the number of parameters, rather than the type of parameters, can do something like:

import cppyy

cppyy.cppdef("""
class MyClass {
public:
    virtual ~MyClass() {}

    virtual std::string ol(int) { return "one int"; }
    virtual std::string ol(int, int) { return "two ints"; }
};

std::string callback_1(MyClass& m) {
    return m.ol(1);
}

std::string callback_2(MyClass& m) {
    return m.ol(1, 1);
}
""")

def single_overload(f):
    def g(self, *args, **kwds):
        try:
            return f(self, *args, **kwds)
        except TypeError as e:
            return getattr(type(self).__bases__[0], f.__name__)(self, *args, **kwds)
    return g

class MyClass(cppyy.gbl.MyClass):
    def ol1(self):
        print("from Python!")

    @single_overload
    def ol(self, t):
        return "python one int"

m = MyClass()

print(cppyy.gbl.callback_1(m))
print(cppyy.gbl.callback_2(m))