Search code examples
pythoninheritancedecoratorpython-decorators

Inheritance with decorators


This question is an extension of a previous question I posted here: link

I use Python 3.6.2. I have a generic class Framework from which my systems inherit.

I use the decorators on_initialize, on_event, and on_finalize to inform the Framework where each method must be executed. The argument precedence of each decorator is used to determine the order of execution for each section (from lower to higher).

# 3 decorators 
def on_initialize(precedence=0):
    def marker(func):
        func._initializer = precedence
        return func
    return marker


def on_event(precedence=0):
    def marker(func):
        func._event_handler = precedence
        return func
    return marker


def on_finalize(precedence=0):
    def marker(func):
        func._finalizer = precedence
        return func
    return marker

# Main framework
class Framework:

    def __init_subclass__(cls, *args, **kw):
        super().__init_subclass__(*args, **kw)
        handlers = dict(_initializer=[], _event_handler=[], _finalizer=[])
        for name, method in cls.__dict__.items():
            for handler_type in handlers:
                if hasattr(method, handler_type):
                    handlers[handler_type].append((getattr(method, handler_type), name))

        for handler_type in handlers:
            setattr(cls, handler_type, 
                    [handler[1] for handler in sorted(handlers[handler_type])])

    def _initialize(self):
        for method_name in self._initializer:
            getattr(self, method_name)()

    def _handle_event(self, event):
        for method_name in self._event_handler:
            getattr(self, method_name)(event)

    def _finalize(self):
        for method_name in self._finalizer:
            getattr(self, method_name)()

    def run(self):
        self._initialize()

        for event in range(10):
            self._handle_event(event)

        self._finalize()


class Recorder(Framework):

    @on_finalize(precedence=0)
    def save_to_db(self):
        print('save_to_db')


class TestFramework(Recorder):

    @on_initialize(precedence=0)
    def get_data(self):
        print('get_data')

    @on_initialize(precedence=1)
    def prepare_data(self):
        print('prepare_data')

    @on_event(precedence=0)
    def process_event(self, event):
        print('process_event', event)

    @on_finalize(precedence=1)
    def generate_report(self):
        print('generate_report')

if __name__ == '__main__':
    tf = TestFramework()
    tf.run()

The results:

> get_data 
> prepare_data 
> process_event 0 
> process_event 1 
> process_event 2
> process_event 3 
> process_event 4 
> process_event 5 
> process_event 6
> process_event 7 
> process_event 8 
> process_event 9 
> generate_report

The method save_to_db from Recorder is not executed in TestFramework. I think I'm missing something in __init_subclass__ where I should be iterating through each subclass. Any idea? Many thanks!


Solution

  • You set a new set of handlers per class, ignoring those of any base class.

    Rather than use empty lists in the handlers dictionary, look for existing lists and start with those:

    def __init_subclass__(cls, *args, **kw):
        super().__init_subclass__(*args, **kw)
        handlers = dict(
            _initializer=getattr(cls, '_initializer', []),
            _event_handler=getattr(cls, '_event_handler', []),
            _finalizer=getattr(cls, '_finalizer', [])))
        # ...
    

    You do then have to eliminate any methods that the subclass has overridden!

    Alternatively, rather than use only the current class namespace (via cls.__dict__), use the combined names available in dir():

    def __init_subclass__(cls, *args, **kw):
        super().__init_subclass__(*args, **kw)
        handlers = dict(_initializer=[], _event_handler=[], _finalizer=[])
        for name in dir(cls):
            method = getattr(cls, name)
            for handler_type in handlers:
                if hasattr(method, handler_type):
                    handlers[handler_type].append((getattr(method, handler_type), name))