'How can decorators be used for callback recognition?

I am implementing a system that uses callbacks for event handling. I currently register the callbacks in a dictionary in each module. For example, see below:

class Module(ABC):
    
    topics = {}
    _eventHandler = None
    
    def __init__(self):
        pass
    
    def onNotify(self, event):
        self.topics[event.topic](event.data)

class Reporter(Module):
    
    def __init__(self):
        self.topics = {'time':self.onMsg,
                       'data':self.onMsg}
    
    def onMsg(self, msg):
        print(f'[reporter] {msg}')
    
    def update(self):
        pass

A collection of Modules is registered in an EventHandler, as seen below:

class EventHandler(queue.Queue):
    
    def __init__(self):
        super().__init__()
        
        self._modules = []
    
    @property
    def modules(self):
        return self._modules
    
    def attach(self, module):
        self._modules.append(module)
        module._eventHandler = self
    
    def detach(self, module):
        self._modules.append(module)
        module._eventHandler = None
    
    def notify(self, event):
        for module in self.modules:
            if event.topic in module.topics.keys():
                module.onNotify(event)
    
    def run(self):
        idx = 0
        while True:
            while not self.empty():
                event = self.get_nowait()
                self.notify(event)
            else:
                self.modules[idx].update()
                idx = (idx + 1) % len(self._modules)

I do not like that there is a separate list to denote callbacks. It seems that the Python developers agree, as PEP 318 states the following in support of decorators: "It also seems less than pythonic to name the function three times for what is conceptually a single declaration."

How can I use decorators to register functions as callbacks that are recognized by the EventHandler, without having a separate list to use as a directory. An example of my desired formatting for the modules is below:

class Module(ABC):
    
    topics = {}
    _eventHandler = None
    
    def __init__(self):
        pass
    
    def onNotify(self, event):
        '''Find the correct callback for event.topic, and direct the event'''

class Reporter(Module):
    
    @callback(topic = ['time', 'data'])
    def onMsg(self, msg):
        print(f'[reporter] {msg}')
    
    def update(self):
        pass

and the EventHandler could use a method like:

def notify(self, event):
    for module in self.modules:
        if event.topic in module.callbacks:
            module.onNotify(event)


Solution 1:[1]

I have determined that using decorators for this would add confusion, and is not a pythonic solution. See my new sample code below that shows an alternate solution.

import time
import threading

class Event():
    
    def __init__(self, topic, payload):
        self.topic = topic
        self.payload = payload

class EventAggregator():
    
    def __init__(self):
        self.subscriber = {}
    
    def publish(self, event):
        if event.topic in self.subscriber.keys():
            filtered_callbacks = self.subscriber[event.topic]
            for callback in filtered_callbacks:
                callback(event)
    
    def subscribe(self, topic, callback):
        if not topic in self.subscriber.keys():
            self.subscriber[topic] = [callback]
        else:
            self.subscriber[topic].append(callback)

class Reporter():
    
    def __init__(self, event_aggregator):
        self._event_aggregator = event_aggregator
        self._event_aggregator.subscribe('data', self.onData)
        self._event_aggregator.subscribe('time', self.onTime)
    
    def onData(self, event):
        print(f'[reporter] (data):{event.payload}')
    
    def onTime(self, event):
        print(f'[reporter] (time):{event.payload}')

class Clock():
    
    def __init__(self, event_aggregator):
        self._event_aggregator = event_aggregator
        
        threading.Thread(target=self.clockProcess).start()
    
    def clockProcess(self):
        while True:
            t = time.time()
            if round(t % 5) == 0:
                event = Event('time', t)
                self.publish(event)
                time.sleep(1)
    
    def publish(self, event):
        self._event_aggregator.publish(event)

if __name__ == '__main__':
    egg = EventAggregator()
    reporter = Reporter(egg)
    clock = Clock(egg)
    
    event = Event('data', 5)
    egg.publish(event)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Deemo