Reputation: 1799
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
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
Reputation: 16728
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
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).
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
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