How to use singledispatchmethod with inheritance class

Viewed 781

In my code, I have the following class:

class A:
  @functools.singledispatchmethod
  def handle(arg):
     pass

I want other class to inherit from A and overload the generic method handle like so:

class B(A):
   @handle.register
   def handle_int(arg: int):
       return arg + 2

However, I get an error:

unresolved reference 'handle'

How can I create this generic method in the base class? (I don't want to create this function in every subclass to use the singledispatchmethod.)

2 Answers

Not ideal approach

Since you are referring to the method defined in class A you have to indicate it using @A.handle.register:

class B(A):
   @A.handle.register
   def handle_int(arg: int):
       return arg + 2
Issue

But this approach causes issues when there is another class C, also inheriting from A but supporting handle(arg: str). Then C().handle(2) will call method from class B since it was registered to A method (even though it should end up in class A base handle method).

Much better approach

The apparent issue with the solution above is one registering class (A), so I am adding registering in all derivative classes but leave the processing to the base class in case there are no proper type-specialized class methods in the derivative classes.

import functools

class A:
  @functools.singledispatchmethod
  def handle(arg):
     print(f'\tA handle (arg: {arg})')

class B(A):
    @functools.singledispatchmethod
    @classmethod
    def handle(cls, arg):
        print(f'\tB handle (arg: {arg})')
        return super(B, cls).handle(arg)


@B.handle.register
def handle_int(arg: int):
    print(f'\tB int (arg: {arg})')
    return arg + 2


class C(A):
    @functools.singledispatchmethod
    @classmethod
    def handle(cls, arg):
        print(f'\tC handle (arg: {arg})')
        return super(C, cls).handle(arg)

@C.handle.register
def handle_str(arg: str):
    print(f'\tC str (arg: {arg})')
    return arg + ' 2'

print('\nA')
A.handle(2)
A.handle('2+')

print('\nB')
B.handle(2)
B.handle('2+')

print('\nC')
C.handle(2)
C.handle('2+')

Result:

A
    A handle (arg: 2)
    A handle (arg: 2+)

B
    B int (arg: 2)
    B handle (arg: 2+)
    A handle (arg: 2+)

C
    C handle (arg: 2)
    A handle (arg: 2)
    C str (arg: 2+)

It's a little difficult to achieve what you're trying to do in Python. singledispatch and singledispatchmethod are both themselves relatively new features in the language. More complex overloading such as what you're attempting isn't particularly well supported at the moment (to my knowledge).

Having said that, you could try the below, using the third-party multipledispatch module. It feels like a little bit of a hack, though, and I'm not sure how to make it work on class methods -- the below solution only works for instance methods.

from multipledispatch import dispatch

@dispatch(object, object)
def handle(instance, arg):
    return 'Base implementation'
        
class A:
    def handle(self, arg):
        return handle(self, arg)

class B(A):
    pass

class C(A):
    pass

@dispatch(B, int)
def handle(instance, arg):
    return 'Specialised implementation for class B and ints'

@dispatch(C, str)
def handle(instance, arg):
    return 'Specialised implementation for class C and strs'

a, b, c = A(), B(), C()

print(a.handle('hi')) # prints "Base implementation"
print(b.handle('hi')) # prints "Base implementation"
print(b.handle(3)) # prints "Specialised implementation for class B and ints"
print(c.handle(3)) # prints "Base implementation"
print(c.handle('hi')) # prints "Specialised implementation for class C and strs"

You might be able to get even closer to your desired result with the plum-dispatch module, another third-party module on pip. I don't know much about it, but I gather it has some extra features that multipledispatch doesn't.

Related