Search code examples
pythonpython-decoratorsclass-variables

Defining a decorator inside a class to affect class variables in a Pythonic way


I'm building a Python class where I want to mark some of the instance methods as 'handlers' that will be applied when the instance is told to run. Below is a minimal working example.

_handlers = []
def handler_marker(marked_fun):
    _handlers.append(marked_fun)
    return marked_fun


class ManyHandler:

    def __init__(self):
        pass

    def run(self, input):
        for handling_fun in _handlers:
            handling_fun(self, input)

    @handler_marker
    def method_1(self, input):
        print(input)

    @handler_marker
    def method_2(self, input):
        print(input[::-1])

    def method_3(self, input):
        print("I'm not marked.")


if __name__ == "__main__":
    MH = ManyHandler()
    MH.run('Test string')

This works, but I don't like that the _handlers list and the decorator are outside of the class definition. Especially the list of handlers would feel a lot more natural as a class variable here. However, if we just move the _handlers inside the class and change the decorator to

def handler_marker(marked_fun):
    ManyHandler._handlers.append(marked_fun)
    return marked_fun

we face one of two problems:

  1. If the decorator is defined before the class, we get NameError: name 'ManyHandler' is not defined since the decorator doesn't know yet what the ManyHandler will be.
  2. If the decorator is defined after the class, we get NameError: name 'handler_marker' is not defined since the class fails to set itself up without knowing what the decorator here is.

So I would like to put the decorator inside of the class as well. But to access the class variable _handlers, the decorator would need to have a reference to the ManyHandler class. We can't pass the class to the decorator as an argument since at the time it decorator is being first applied, we're in the process of still building the class.

So far the only reasonable solution I've found has been to set up a new class to handle the handlers:

class HandlerHandler:
    _handlers = []

    def __init__(self):
        pass

    @classmethod
    def handler_marker(cls, marked_fun):
        cls._handlers.append(marked_fun)
        return marked_fun


class ManyHandler:
    MetaHandler = HandlerHandler
    _handlers = MetaHandler._handlers
    handler_marker = MetaHandler.handler_marker

    def __init__(self):
        pass

    def run(self, input):
        for handling_fun in type(self)._handlers:
            handling_fun(self, input)

    @handler_marker
    def method_1(self, input):
        print(input)

    @handler_marker
    def method_2(self, input):
        print(input[::-1])

    def method_3(self, input):
        print("I'm not marked.")


if __name__ == "__main__":
    MH = ManyHandler()
    MH.run('Test string')

It works, but I'm not convinced if this is Pythonic (or even sane) anymore.

So my question is:

What would be the most Pythonic way to do this?


Solution

  • Creating a separate class is totally sane -- that's what classes are for, for encapsulating state and behavior. However, here's how I would clean up this approach:

    class HandlerRegistry:
        def __init__(self):
            self._handlers = []
        def register(self, fun):
            self._handlers.append(fun)
            return fun
        def __iter__(self):
            yield from self._handlers
    
    class ManyHandler:
        handlers = HandlerRegistry()
    
        def run(self, input):
            for handling_fun in self.handlers:
                handling_fun(self, input)
    
        @handlers.register
        def method_1(self, input):
            print(input)
    
        @handlers.register
        def method_2(self, input):
            print(input[::-1])
    
        def method_3(self, input):
            print("I'm not marked.")
    
    mh = ManyHandler()
    mh.run('Test string')