Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon

Python Design Patterns in Depth – The Observer Pattern

Save for later
  • 11 min read
  • 16 Feb 2016

article-image

In this atricle you will see a group of objects when the state of another object changes. A very popular example lies in the Model-View-Controller (MVC) pattern. Assume that we are using the data of the same model in two views, for instance in a pie chart and in a spreadsheet. Whenever the model is modified, both the views need to be updated. That's the role of the Observer design pattern [Eckel08, page 213].

The Observer pattern describes a publish-subscribe relationship between a single object, : the publisher, which is also known as the subject or observable, and one or more objects, : the subscribers, also known as observers. In the MVC example, the publisher is the model and the subscribers are the views. However, MVC is not the only publish-subscribe example. Subscribing to a news feed such as RSS or Atom is another example. Many readers can subscribe to the feed typically using a feed reader, and every time a new item is added, they receive the update automatically.

The ideas behind Observer are the same as the ideas behind MVC and the separation of concerns principle, that is, to increase decoupling between the publisher and subscribers, and to make it easy to add/remove subscribers at runtime. Additionally, the publisher is not concerned about who its observers are. It just sends notifications to all the subscribers [GOF95, page 327].

(For more resources related to this topic, see here.)

A real-life example

In reality, an auction resembles Observer. Every auction bidder has a number paddle that is raised whenever they want to place a bid. Whenever the paddle is raised by a bidder, the auctioneer acts as the subject by updating the price of the bid and broadcasting the new price to all bidders (subscribers).

The following figure, courtesy of www.sourcemaking.com, [j.mp/observerpat], shows how the Observer pattern relates to an auction:

python-design-patterns-depth-observer-pattern-img-0

A software example

The django-observer package [j.mp/django-obs] is a third-party Django package that can be used to register callback functions that are executed when there are changes in several Django fields. Many different types of fields are supported (CharField, IntegerField, and so forth).

RabbitMQ is a library that can be used to add asynchronous messaging support to an application. Several messaging protocols are supported, such as HTTP and AMQP. RabbitMQ can be used in a Python application to implement a publish-subscribe pattern, which is nothing more than the Observer design pattern [j.mp/rabbitmqobs].

Use cases

We generally use the Observer pattern when we want to inform/update one or more objects (observers/subscribers) about a change that happened to another object (subject/publisher/observable). The number of observers as well as who the observers are may vary and can be changed dynamically (at runtime).

We can think of many cases where Observer can be useful. Whether it is RSS, Atom, or another format, the idea is the same; you follow a feed, and every time it is updated, you receive a notification about the update [Zlobin13, page 60].

The same concept exists in social networking. If you are connected to another person using a social networking service, and your connection updates something, you are notified about it. It doesn't matter if the connection is a Twitter user that you follow, a real friend on Facebook, or a business colleague on LinkedIn.

Event-driven systems are another example where Observer can be (and usually is) used. In such systems, listeners are used to "listen" for specific events. The listeners are triggered when an event they are listening to is created. This can be typing a specific key (of the keyboard), moving the mouse, and more. The event plays the role of the publisher and the listeners play the role of the observers. The key point in this case is that multiple listeners (observers) can be attached to a single event (publisher) [j.mp/magobs].

Implementation

We will implement a data formatter. The ideas described here are based on the ActiveState Python Observer code recipe [j.mp/pythonobs]. There is a default formatter that shows a value in the decimal format. However, we can add/register more formatters. In this example, we will add a hex and binary formatter. Every time the value of the default formatter is updated, the registered formatters are notified and take action. In this case, the action is to show the new value in the relevant format.

Observer is actually one of the patterns where inheritance makes sense. We can have a base Publisher class that contains the common functionality of adding, removing, and notifying observers. Our DefaultFormatter class derives from Publisher and adds the formatter-specific functionality. We can dynamically add and remove observers on demand. The following class diagram shows an instance of the example using two observers: HexFormatter and BinaryFormatter. Note that, because class diagrams are static, they cannot show the whole lifetime of a system, only the state of it at a specific point in time.

We begin with the Publisher class. The observers are kept in the observers list. The add() method registers a new observer, or throws an error if it already exists. The remove() method unregisters an existing observer, or throws an exception if it does not exist. Finally, the notify() method informs all observers about a change:

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at ₹800/month. Cancel anytime
class Publisher:
   def __init__(self):
       self.observers = []

   def add(self, observer):
       if observer not in self.observers:
           self.observers.append(observer)
       else:
           print('Failed to add: {}'.format(observer))

   def remove(self, observer):
       try:
           self.observers.remove(observer)
       except ValueError:
           print('Failed to remove: {}'.format(observer))

   def notify(self):
       [o.notify(self) for o in self.observers]

Let's continue with the DefaultFormatter class. The first thing that __init__() does is call __init__() method of the base class, since this is not done automatically in Python. A DefaultFormatter instance has name to make it easier for us to track its status. We use name mangling in the _data variable to state that it should not be accessed directly. Note that this is always possible in Python [Lott14, page 54] but fellow developers have no excuse for doing so, since the code already states that they shouldn't. There is a serious reason for using name mangling in this case. Stay tuned. DefaultFormatter treats the _data variable as an integer, and the default value is zero:

class DefaultFormatter(Publisher):
   def __init__(self, name):
       Publisher.__init__(self)
       self.name = name
       self._data = 0

The __str__() method returns information about the name of the publisher and the value of _data. type(self).__name__ is a handy trick to get the name of a class without hardcoding it. It is one of those things that make the code less readable but easier to maintain. It is up to you to decide if you like it or not:

def __str__(self):
       return "{}: '{}' has data = {}".format(type(self).__name__, self.name,
                                                                    self._data)

There are two data() methods. The first one uses the @property decorator to give read access to the _data variable. Using this, we can just execute object.data instead of object.data():

   @property
   def data(self):
       return self._data

The second data() method is more interesting. It uses the @setter decorator, which is called every time the assignment (=) operator is used to assign a new value to the _data variable. This method also tries to cast a new value to an integer, and does exception handling in case this operation fails:

   @data.setter
   def data(self, new_value):
       try:
           self._data = int(new_value)
       except ValueError as e:
            print('Error: {}'.format(e))
       else:
           self.notify()

The next step is to add the observers. The functionality of HexFormatter and BinaryFormatter is very similar. The only difference between them is how they format the value of data received by the publisher, that is, in hexadecimal and binary, respectively:

class HexFormatter:
   def notify(self, publisher):
       print("{}: '{}' has now hex data = {}".format(type(self).__name__,
                 publisher.name, hex(publisher.data)))
class BinaryFormatter:
   def notify(self, publisher):
       print("{}: '{}' has now bin data = {}".format(type(self).__name__,
                 publisher.name, bin(publisher.data)))

No example is fun without some test data. The main() function initially creates a DefaultFormatter instance named test1 and afterwards attaches (and detaches) the two available observers. Exception handling is also exercised to make sure that the application does not crash when erroneous data is passed by the user. Moreover, things such as trying to add the same observer twice or removing an observer that does not exist should cause no crashes:

def main():
   df = DefaultFormatter('test1')
   print(df)

   print()
   hf = HexFormatter()
   df.add(hf)
 df.data = 3
   print(df)

   print()
   bf = BinaryFormatter()
   df.add(bf)
   df.data = 21
   print(df)

   print()
   df.remove(hf)
   df.data = 40
   print(df)

   print()
   df.remove(hf)
   df.add(bf)
   df.data = 'hello'
   print(df)

   print()
   df.data = 15.8
   print(df)

Here's how the full code of the example (observer.py) looks:

class Publisher:
   def __init__(self):
       self.observers = []

   def add(self, observer):
       if observer not in self.observers:
           self.observers.append(observer)
       else:
           print('Failed to add: {}'.format(observer))

   def remove(self, observer):
       try:
           self.observers.remove(observer)
       except ValueError:
          print('Failed to remove: {}'.format(observer))

   def notify(self):
       [o.notify(self) for o in self.observers]

class DefaultFormatter(Publisher):
   def __init__(self, name):
       Publisher.__init__(self)
       self.name = name
       self._data = 0

   def __str__(self):
       return "{}: '{}' has data = {}".format(type(self).__name__, self.name, self._data)

   @property
   def data(self):
       return self._data

   @data.setter
   def data(self, new_value):
       try:
           self._data = int(new_value)
       except ValueError as e:
           print('Error: {}'.format(e))
       else:
           self.notify()

class HexFormatter:
   def notify(self, publisher):
       print("{}: '{}' has now hex data = {}".format(type(self).__name__, publisher.name, hex(publisher.data)))

class BinaryFormatter:
   def notify(self, publisher):
       print("{}: '{}' has now bin data = {}".format(type(self).__name__, publisher.name, bin(publisher.data)))

def main():
   df = DefaultFormatter('test1')
   print(df)

   print()
   hf = HexFormatter()
   df.add(hf)
   df.data = 3
   print(df)

   print()
   bf = BinaryFormatter()
   df.add(bf)
   df.data = 21
   print(df)

   print()
   df.remove(hf)
   df.data = 40
   print(df)

   print()
   df.remove(hf)
   df.add(bf)

   df.data = 'hello'
   print(df)

   print()
   df.data = 15.8
   print(df)

if __name__ == '__main__':
   main()

Executing observer.py gives the following output:

>>> python3 observer.py
DefaultFormatter: 'test1' has data = 0

HexFormatter: 'test1' has now hex data = 0x3
DefaultFormatter: 'test1' has data = 3

HexFormatter: 'test1' has now hex data = 0x15
BinaryFormatter: 'test1' has now bin data = 0b10101
DefaultFormatter: 'test1' has data = 21

BinaryFormatter: 'test1' has now bin data = 0b101000
DefaultFormatter: 'test1' has data = 40
Failed to remove: <__main__.HexFormatter object at 0x7f30a2fb82e8>
Failed to add: <__main__.BinaryFormatter object at 0x7f30a2fb8320>
Error: invalid literal for int() with base 10: 'hello'
BinaryFormatter: 'test1' has now bin data = 0b101000
DefaultFormatter: 'test1' has data = 40

BinaryFormatter: 'test1' has now bin data = 0b1111
DefaultFormatter: 'test1' has data = 15 

What we see in the output is that as the extra observers are added, more (and relevant) output is shown, and when an observer is removed, it is not notified any longer. That's exactly what we want: runtime notifications that we are able to enable/disable on demand.

The defensive programming part of the application also seems to work fine. Trying to do funny things such as removing an observer that does not exist or adding the same observer twice is not allowed. The messages shown are not very user-friendly but I leave that up to you as an exercise. Runtime failures of trying to pass a string when the API expects a number are also properly handled without causing the application to crash/terminate.

This example would be much more interesting if it were interactive. Even a simple menu that allows the user to attach/detach observers at runtime and modify the value of DefaultFormatter would be nice because the runtime aspect becomes much more visible. Feel free to do it.

Another nice exercise is to add more observers. For example, you can add an octal formatter, a roman numeral formatter, or any other observer that uses your favorite representation. Be creative and have fun!

Summary

In this article, we covered the Observer design pattern. We use Observer when we want to be able to inform/notify all stakeholders (an object or a group of objects) when the state of an object changes. An important feature of observer is that the number of subscribers/observers as well as who the subscribers are may vary and can be changed at runtime.

You can refer more books on this topic mentioned as follows:

  • Expert Python Programming: https://www.packtpub.com/application-development/expert-python-programming
  • Learning Python Design Patterns: https://www.packtpub.com/application-development/learning-python-design-patterns

Resources for Article:


Further resources on this subject: