Search code examples
pythonoverloadingabstract-methodsgeneric-functionsingle-dispatch

Abstract method with single-dispatch generic functions


I want to put single-dispatch method in functools module with abstract method in abc module in one method in my abstract class and then using the method in my subclasses based on first argument type. but the problem is the dispatch method doesn't works.

class Abstract(metaclass=ABCMeta):
    ...
    ...
    ...

    @singledispatchmethod 
    @abstractmethod
    def apply(self, element:str):
        raise NotImplementedError

   @apply.register
   @abstractmethod
   def _(self, element: int):
       raise NotImplementedError


class A(Abstract):
    def apply(self, element: str):
        print(f'this is string -> {element}')

    def _(self, element: int):
        print(f'this is intiger -> {element}')


>>>instance = A()
>>>instance.apply(2)
#will return (this is string -> 2)

i have solved this problem in other way but i'm curious about this problem if it has an answer


Solution

  • 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