Yedidya kfir
Yedidya kfir

Reputation: 1799

How to use singledispatchmethod with inheritance class

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

Upvotes: 10

Views: 2874

Answers (3)

Michał Góral
Michał Góral

Reputation: 1517

I had a similar problem and encountered the limitation of @singledispatchmethod which @sophros mentioned: when many classes inherit a single base class, they "leak" implementations of dispatched methods between them.

Coming from C++ land, I was inspired by Herb Sutter's "Non-virtual interface" idiom (http://www.gotw.ca/publications/mill18.htm), which argues that public interfaces should be stable and predictable and inheritance-based function overloading is best left as implementation detail, invisible at the interface level.

In this spirit my approach is to leave dispatching logic in a public interface, implemented in a base class, which then delegates work to the methods implemented in subclasses.

from abc import ABCMeta, abstractmethod
from functools import singledispatchmethod


class Base(metaclass=ABCMeta):
    @singledispatchmethod
    def handle(self, arg):
        raise TypeError("type without implemented handle")

    @handle.register
    def _(self, arg: int):
        return self._handle_int(arg)

    @handle.register
    def _(self, arg: str):
        return self._handle_str(arg)

    @abstractmethod
    def _handle_int(self, arg: int): ...

    @abstractmethod
    def _handle_str(self, arg: str): ...


class A(Base):
    def _handle_int(self, arg: int):
        print(f"A int {arg}")

    def _handle_str(self, arg: str):
        print(f"A str {arg}")


class B(Base):
    def _handle_int(self, arg: int):
        print(f"B int {arg}")

    def _handle_str(self, arg: str):
        print(f"B str {arg}")

class C(Base):
    def _handle_int(self, arg: int):
        print(f"B int {arg}")


A().handle(1)
A().handle("foo")
B().handle(1)
B().handle("foo")
C().handle(1)
C().handle("foo")

Output of above snippet is:

A int 1
A str foo
B int 1
B str foo
Traceback (most recent call last):
  File "file.py", line 49, in <module>
    C().handle(1)
    ^^^
TypeError: Can't instantiate abstract class C with abstract method _handle_str

Upvotes: 1

sophros
sophros

Reputation: 16728

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

Upvotes: 3

Alex Waygood
Alex Waygood

Reputation: 7569

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.

Upvotes: 2

Related Questions