7

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.)

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Yedidya kfir
  • 1,419
  • 3
  • 17
  • 32
  • Is `handle` an instance, static or class method? Also: are there ever any direct instances of `A`, or is it more of an abstract base class, i.e., just there so that other classes can inherit from it? – Alex Waygood Jun 30 '21 at 08:24
  • 1
    Handle is a class method and there are instances of A – Yedidya kfir Jun 30 '21 at 12:52
  • And to be clear, you're looking for an implementation where you can have a "fallback" implementation in the base class, and multiple implementations in each subclass — e.g. both an int implementation and a str implementation in Class B, and (for example), a list implementation and a str implementation in Class C, with both C and A inheriting the fallback generic function from A but not sharing their specialised implementations with each other? – Alex Waygood Jun 30 '21 at 16:24
  • 1
    Yes, you are exactly right. Imagine function overload in c#.... – Yedidya kfir Jun 30 '21 at 17:18

2 Answers2

3

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+)
sophros
  • 14,672
  • 11
  • 46
  • 75
  • 1
    This won't work, say I also have a class C inherit from A and the data type for C handle func is str. Now if I send C().handle(2) it will end up in the method from class B since it was register to A method (even though it should end up in class A base handle func) – Yedidya kfir Jun 30 '21 at 12:46
  • The new solution beats the purpose of inheritance since I need to redclare the handle function for each class.... – Yedidya kfir Jul 02 '21 at 07:02
  • 1
    True. As it was pointed out by @alex-waygood, the support from the `functools`' `singledispatchmethod` is rather limited. – sophros Jul 02 '21 at 07:44
  • This approach violates the DRY principle. Unfortunately, some features are not properly designed, and yet, are being rushed into the language. Following this trajectory, Python 4.0 becomes an inevitability which is very hurtful for the evolution of the language. – 303 May 20 '22 at 11:50
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.

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46