~/Blog

Brandon Rozek

Photo of Brandon Rozek

PhD Student @ RPI studying Automated Reasoning in AI and Linux Enthusiast.

Python Patterns: Subscribe

Published on

Updated on

3 minute reading time

It is common for larger applications to have modules that publishes and subscribes to events. This post will outline a couple ways to achieve this using decorators and standard methods.

Single Event

First let us concern ourselves with a single event since that’s the easiest. Here we will create an application class that stores callbacks of functions through the subscribe decorator. Calling emit will send a message to all the functions stored in self.callbacks.

from typing import Callable, List
class Application:
    def __init__(self):
        self.callbacks: List[Callable] = []
    def subscribe(self, func: Callable):
        if not callable(func):
            raise ValueError("Argument func must be callable.")
        self.callbacks.append(func)
        return func
    def emit(self, message):
        for callback in self.callbacks:
            callback(message)

Here is an example of its usage:

app = Application()

@app.subscribe
def test1(message):
    print("Function 1:", message)

def test2(message):
    print("Function 2:", message)

app.subscribe(test2)

app.emit('Hello World')
Function 1: Hello World
Function 2: Hello World

Multiple Events

Let’s say you want the application to handle different types of events. Now self.callbacks is a dictionary of lists, where the key is the event and the list is the same as the last section. There’s an additional layered function on top of subscribe this time in order to handle passing an argument into the decorator.

from collections import defaultdict
from typing import Callable, Optional
class Application:
    def __init__(self):
        self.callbacks: Dict[str, List[Callable]] = defaultdict(list)
    def on(self, event: str, func: Optional[Callable] = None):
        def subscribe(func: Callable):
            if not callable(func):
                raise ValueError("Argument func must be callable.")
            self.callbacks[event].append(func)
            return func
        if func is None:
            return subscribe
        subscribe(func)
    def emit(self, event, message):
        for callback in self.callbacks[event]:
            callback(message)

To show its usage lets first create an instance of Application

app = Application()

Now let’s subscribe a couple functions to event1

@app.on('event1')
def test1(message):
    print("Function 1:", message)

def test3(message):
    print("Function 3:", message)

app.on('event1', test3)

Now to subscribe a couple events to event2

# Subscribed to event 2
@app.on('event2')
def test2(message):
    print("Function 2:", message)

def test4(message):
    print("Function 4:", message)

app.on('event2', test4)

We can also subscribe to both events

# Subscribed to both events
@app.on('event1')
@app.on('event2')
def test5(message):
    print("Function 5:", message)
app.emit('event1', 'Hello, World!')
Function 1: Hello, World!
Function 3: Hello, World!
Function 5: Hello, World!
app.emit('event2', 'Goodbye, World!')
Function 2: Goodbye, World!
Function 4: Goodbye, World!
Function 5: Goodbye, World!

Alternative: Observer Pattern

The above approaches assume that the callbacks would be needed for the lifetime of the program. An altnerative is to use the Observer design pattern.

This assumes that we have two classes, Observable and Observer. The latter class implements the notify method and when it is constructed, it adds itself to the collection contained in the Observable class.

Martin wrote a great blog post on how you can use Python weak references so that when your Observer class goes out of scope, a copy isn’t stored in the Observable class and instead gets safely removed from the collection.

Reply via Email Buy me a Coffee
Was this useful? Feel free to share: Hacker News Reddit Twitter