Implementations
I was curious to find this out, because I just worked on a project where I wanted to have this feature, because I was porting an app from C# where I could simply use overloads. I got this behaviour to work with two different approaches:
Derived class dispatch
This is the version that I needed, which makes the dispatchmethod abstract and every derived class thus also has to define a concrete dispatchmethod with the same signature (important: these are now separate dispatch methods and can provide different registrations). Also note that, because these are concrete, every derived class has their own dispatch and the function in the DoerBase
is never called (as it is only abstract).
from abc import ABC, abstractmethod
from functools import singledispatchmethod
from typing import Any
from typing_extensions import override
class DoerBase(ABC):
@singledispatchmethod
@abstractmethod
def do_something(self, arg: Any) -> None: ...
class IntDoer(DoerBase):
@singledispatchmethod
@override
def do_something(self, arg: Any) -> None:
raise NotImplementedError(f"This {type(self).__name__} cannot do anything with a {type(arg).__name__}!")
@do_something.register
def _(self, arg: int):
print("The number", arg, "is half of", 2 * arg)
class StringDoer(DoerBase):
@singledispatchmethod
@override
def do_something(self, arg: Any) -> None:
raise NotImplementedError(f"This {type(self).__name__} cannot do anything with a {type(arg).__name__}!")
@do_something.register
def _(self, arg: str):
print("I can print this string for you:", arg)
def main():
int_doer = IntDoer()
string_doer = StringDoer()
int_doer.do_something(321)
string_doer.do_something("Hello")
# This IntDoer cannot do anything with a str!
# int_doer.do_something("Hello")
# This StringDoer cannot do anything with a int!
# string_doer.do_something(321)
if __name__ == "__main__":
main()
Base class dispatch
The version which is more similar to the one in your example declares the registered dispatch types in the base class (the above method declares registrations per derived class). Now every base class must override the abstract dispatch registrations. I was able to recreate this behaviour by calling an abstract method from the registered dispatch handler instead of trying to make the dispatch handler itself abstract.
from abc import ABCMeta, abstractmethod
from functools import singledispatchmethod
from typing import Any
class ABase(metaclass=ABCMeta):
@singledispatchmethod
def apply(self, element: Any) -> None: ...
@apply.register
def _(self, element: int): return self.apply_int(element)
@abstractmethod
def apply_int(self, element: int) -> None: ...
class A(ABase):
def apply_int(self, element: int):
print("I applied the number", element)
class B(ABase): pass
instance = A()
instance.apply(2)
#will print "I applied the number 2"
# b_instance = B()
# TypeError: Can't instantiate abstract class B with abstract method apply_int
Conclusions:
- On your derived class you definitely also need to provide the
@singledispatchmethod
decorator
- It seems that you cannot combine the
@method.register
decorator with the @abstractmethod
decorator
- You can circumvent this behaviour by careful placement of the
@abstractmethod
decorator
- The two above implementations differ in their behaviour: one declares that the class has a dispatch method, that each subclass has to declare and register, while the other implementation defines the dispatch registrations that all subclasses have to fulfill