Tutorial: The Observer Pattern in Python



Part 1: Introduction

I'm Aaron Maxwell, an instructor at ProTech and author of the Advanced Python Newsletter. In this short tutorial we're going to talk about the observer pattern in Python.

Like many design patterns, the Observer in Python is different. A lot has been written about patterns since the turn of the century, for languages like Java, C++, C# and PHP. Curiously, though, much of that doesn't exactly apply to Python - or, at least, applies in a different way. Dynamic typing, functions as first-class objects, and some other additions to the object model all mean design patterns just work differently in this language.

Part 2: The Simple Observer

The simplest form of the Pythonic observer works much like in other languages. There's an opportunity to think about it more clearly, though. Instead of the common terminology "observable" and "observers", it's helpful to think in terms of a "publisher" and "subscribers". At its heart, pub-sub is all the observer pattern really is!

Example 1: observer1.py

class Subscriber:
    def __init__(self, name):
        self.name = name
    def update(self, message):
        print('{} got message "{}"'.format(self.name, message))
        
class Publisher:
    def __init__(self):
        self.subscribers = set()
    def register(self, who):
        self.subscribers.add(who)
    def unregister(self, who):
        self.subscribers.discard(who)
    def dispatch(self, message):
        for subscriber in self.subscribers:
            subscriber.update(message)

Example 2: driver_observer1.py

from observer1 import Publisher, Subscriber

pub = Publisher()

bob = Subscriber('Bob')
alice = Subscriber('Alice')
john = Subscriber('John')

pub.register(bob)
pub.register(alice)
pub.register(john)

pub.dispatch("It's lunchtime!")

pub.unregister(john)

pub.dispatch("Time for dinner")

 

Note: All of these code examples are also available on my GitHub page

Part 3: A Pythonic Refinement

The observer pattern's traditional form relies on both the observable (publisher) and observer (subscriber) to agree on a common subscriber method. When a noteworthy event is triggered, the subscriber calls the method of that name. Python lets us be much more flexible; we can easily implement the pattern so observers can register any callback method they want.

Example 1: observer2.py

class SubscriberOne:
    def __init__(self, name):
        self.name = name
    def update(self, message):
        print('{} got message "{}"'.format(self.name, message))
class SubscriberTwo:
    def __init__(self, name):
        self.name = name
    def receive(self, message):
        print('{} got message "{}"'.format(self.name, message))
        
class Publisher:
    def __init__(self):
        self.subscribers = dict()
    def register(self, who, callback=None):
        if callback == None:
            callback = getattr(who, 'update')
        self.subscribers[who] = callback
    def unregister(self, who):
        del self.subscribers[who]
    def dispatch(self, message):
        for subscriber, callback in self.subscribers.items():
            callback(message)

Example 2: driver_observer2.py

from observer2 import Publisher, SubscriberOne, SubscriberTwo

pub = Publisher()
bob = SubscriberOne('Bob')
alice = SubscriberTwo('Alice')
john = SubscriberOne('John')

pub.register(bob, bob.update)
pub.register(alice, alice.receive)
pub.register(john)

pub.dispatch("It's lunchtime!")
pub.unregister(john)
pub.dispatch("Time for dinner")

Part 4: Observing Events

Python gives us rich expressiveness in how we implement the Observer pattern. This really shines through when we let observers subscribe to different kinds of events. Combining Python's functional power with its flexible built-in collection types, we can implement a multi-event observable with concise, clear, maintainable code.

Example 1: observer3.py

class Subscriber:
    def __init__(self, name):
        self.name = name
    def update(self, message):
        print('{} got message "{}"'.format(self.name, message))
        
class Publisher:
    def __init__(self, events):
        # maps event names to subscribers
        # str -> dict
        self.events = { event : dict()
                          for event in events }
    def get_subscribers(self, event):
        return self.events[event]
    def register(self, event, who, callback=None):
        if callback == None:
            callback = getattr(who, 'update')
        self.get_subscribers(event)[who] = callback
    def unregister(self, event, who):
        del self.get_subscribers(event)[who]
    def dispatch(self, event, message):
        for subscriber, callback in self.get_subscribers(event).items():
            callback(message)

Example 2: driver_observer3.py

from observer3 import Publisher, Subscriber

pub = Publisher(['lunch', 'dinner'])
bob = Subscriber('Bob')
alice = Subscriber('Alice')
john = Subscriber('John')

pub.register("lunch", bob)
pub.register("dinner", alice)
pub.register("lunch", john)
pub.register("dinner", john)

pub.dispatch("lunch", "It's lunchtime!")
pub.dispatch("dinner", "Dinner is served")

Part 5: Conclusion: Code, Resources, & More

So that's the observer pattern in Python. We've seen all the ways that you can implement these design patterns and the great flexibility we have. Now, in closing, I want to point to a few resources.

Additional Resources

Published July 6, 2015