Search code examples
pythoninheritanceabstract

why implement abstractmethod as staticmethod


I'm learning python design patterns from github repo faif/python-patterns and found the example chain_of_responsibility implements abstractmethod check_range as staticmethod.

My question is, is there any benefit other than less typing a self?

Simplify code is

from abc import ABC, abstractmethod


class A(ABC):
    @abstractmethod
    def foo(self, x):
        pass


class B(A):
    @staticmethod
    def foo(x):
        print("B.foo", x)


# both the two works
B.foo(1)

b = B()
b.foo(2)

Solution

  • There's no particular benefit. Sure, not all the check_range methods need an instance of the handler class that defines it, so they are declared as static methods.

    But there's no reason any of the classes in your linked page need to exist in the first place, because in Python you can just store a function itself in a list, rather than storing some other dummy object that has an equivalent method.

    Here's how you can implement "chain of responsibility" in Python idiomatically.

    def check_range0(request):
        return request in range(10)
    
    # A closure can be used in place of a class
    def make_check_range1():
        start = 10
        stop = 20
        return lambda request: request in range(start, stop)
    
    # Another way of using a closure in place of a class
    def make_check_range2(start, stop):
        return lambda request: request in range(start, stop)
    
    
    def fallback(request):
        print("No handler for {request}")
    
    handlers = [check_range0, make_check_range1(), make_check_range2(20, 30)]
    
    
    for request in requests:
        if any((handler:=f)(request) for f in handlers):
            print(f"{request} handled by {handler.__name__}")
        else:
            fallback(request)
    

    A simple list takes the place of the linked list implied by Handler. Each subclass of Handler is replaced by a regular function (or a function that returns a closure, just to emphasize that a class is not necessary to provide or store state). The any function implements the iteration provided by Handler.handle.

    If you really want a handler class, you can define it more simply than the example.

    class Handler(ABC):
        def __init__(self, handlers=None):
            if handlers is None:
                handlers = []
            self.handlers = handlers
            self.fallback = lambda request: pass
    
        def add_handler(self, f):
            self.handlers.append(f)
    
        # Barely necessary; you can set the fallback
        # attribute on a Handler instance yourself.
        def set_fallback(self, f):
            self.fallback = f
    
        def handle(self, request):
            # An alternative to any()
            for h in self.handlers:
                if h(request):
                    break
            else:
                self.fallback(request)
    
    h = Handler([check_range0])
    h.add_handler(make_check_range1())
    h.add_handler(make_check_range2(20, 30))
    
    def fallback(request):
        print(f"No handler for {request}"
    
    h.fallback = fallback
    # h.set_fallback(fallback)
    
    for request in requests:
        h.handle(request)