Behavioral patterns

Behavioral patterns are intended to simplify the interactions between classes by structuring the processes of their interaction.

This section provides three examples of popular behavioral patterns that you may want to consider when writing Python code:

  • Observer
  • Visitor
  • Template

Observer

The observer pattern is used to notify a list of objects about a state change of the observed component.

Observer allows adding features in an application in a pluggable way by de-coupling the new functionality from the existing code base. An event framework is a typical implementation of the observer pattern and is described in the figure that follows. Every time an event occurs, all observers for this event are notified with the subject that has triggered this event.

An event is created when something happens. In graphical user interface applications, event-driven programming (see http://en.wikipedia.org/wiki/Event-driven_programming) is often used to link the code to user actions. For instance, a function can be linked to the MouseMove event so it is called every time the mouse moves over the window.

In case of GUI application, de-coupling the code from the window management internals simplifies the work a lot. Functions are written separately and then registered as event observers. This approach exists from the earliest versions of Microsoft's MFC framework (see http://en.wikipedia.org/wiki/Microsoft_Foundation_Class_Library) and in all GUI development tools such as Qt or GTK. Many frameworks use the notion of signals, but they are simply another manifestation of the observer pattern.

The code can also generate events. For instance, in an application that stores documents in a database, DocumentCreated, DocumentModified, and DocumentDeleted can be three events provided by the code. A new feature that works on documents can register itself as an observer to get notified every time a document is created, modified, or deleted and do the appropriate work. A document indexer could be added that way in an application. Of course, this requires that all the code in charge of creating, modifying, or deleting documents is triggering events. But this is rather easier than adding indexing hooks all over the application code base! A popular web framework that follows this pattern is Django with its mechanism of signals.

An Event class can be implemented for the registration of observers in Python by working at the class level:

class Event:
    _observers = []

    def __init__(self, subject):
        self.subject = subject

    @classmethod
    def register(cls, observer):
        if observer not in cls._observers:
            cls._observers.append(observer)

    @classmethod
    def unregister(cls, observer):
        if observer in cls._observers:
            cls._observers.remove(observer)

    @classmethod
    def notify(cls, subject):
        event = cls(subject)
        for observer in cls._observers:
            observer(event)

The idea is that observers register themselves using the Event class method and get notified with Event instances that carry the subject that triggered them. Here is an example of the concrete Event subclass with some observers subscribed to its notifications:

class WriteEvent(Event):
    def __repr__(self):
        return 'WriteEvent'


def log(event):
    print(
        '{!r} was fired with subject "{}"'
        ''.format(event, event.subject)
    )


class AnotherObserver(object):
    def __call__(self, event):
        print(
            "{!r} trigerred {}'s action"
            "".format(event, self.__class__.__name__)
        )


WriteEvent.register(log)
WriteEvent.register(AnotherObserver())

And here is an example result of firing the event with the WriteEvent.notify() method:

>>> WriteEvent.notify("something happened")
WriteEvent was fired with subject "something happened"
WriteEvent trigerred AnotherObserver's action

This implementation is simple and serves only as illustrational purposes. To make it fully functional, it could be enhanced by:

  • Allowing the developer to change the order or events
  • Making the event object hold more information than just the subject

De-coupling your code is fun and the observer is the right pattern to do it. It componentizes your application and makes it more extensible. If you want to use an existing tool, try Blinker (refer to https://pythonhosted.org/blinker/). It provides fast and simple object-to-object and broadcast signaling for Python objects.

Visitor

Visitor helps in separating algorithms from data structures and has a similar goal to that of the observer pattern. It allows extending the functionalities of a given class without changing its code. But the visitor goes a bit further by defining a class that is responsible for holding data and pushes the algorithms to other classes called Visitors. Each visitor is specialized in one algorithm and can apply it on the data.

This behavior is quite similar to the MVC paradigm (refer to http://en.wikipedia.org/wiki/Model-view-controller), where documents are passive containers pushed to views through controllers, or where models contain data that is altered by a controller.

Visitor pattern is implemented by providing an entry point in the data class that can be visited by all kinds of visitors. A generic description is a Visitable class that accepts Visitor instances and calls them, as shown in the following figure:

Visitor

The Visitable class decides how it calls the Visitor class, for instance, by deciding which method is called. For example, a visitor in charge of printing built-in type content can implement the visit_TYPENAME() methods, and each of these types can call the given method in its accept() method:

class VisitableList(list):
    def accept(self, visitor):
        visitor.visit_list(self)


class VisitableDict(dict):
    def accept(self, visitor):
        visitor.visit_dict(self)


class Printer(object):
    def visit_list(self, instance):
        print('list content: {}'.format(instance))

    def visit_dict(self, instance):
        print('dict keys: {}'.format(
            ', '.join(instance.keys()))
        )

This is done as shown in the following example:

>>> visitable_list = VisitableList([1, 2, 5])
>>> visitable_list.accept(Printer())
list content: [1, 2, 5]
>>> visitable_dict = VisitableDict({'one': 1, 'two': 2, 'three': 3})
>>> visitable_dict.accept(Printer())
dict keys: two, one, three

But this pattern means that each visited class needs to have an accept method to be visited, which is quite painful.

Since Python allows code introspection, a better idea is to automatically link visitors and visited classes:

>>> def visit(visited, visitor):
...     cls = visited.__class__.__name__
...     method_name = 'visit_%s' % cls
...     method = getattr(visitor, method_name, None)
...     if isinstance(method, Callable):
...         method(visited)
...     else:
...         raise AttributeError(
...             "No suitable '{}' method in visitor"
...             "".format(method_name)
...         )
... 
>>> visit([1,2,3], Printer())
list content: [1, 2, 3]
>>> visit({'one': 1, 'two': 2, 'three': 3}, Printer())
dict keys: two, one, three
>>> visit((1, 2, 3), Printer())
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 10, in visit
AttributeError: No suitable 'visit_tuple' method in visitor

This pattern is used in this way in the ast module, for instance, by the NodeVisitor class that calls the visitor with each node of the compiled code tree. This is because Python doesn't have a match operator like Haskell.

Another example is a directory walker that calls Visitor methods depending on the file extension:

>>> def visit(directory, visitor):
...     for root, dirs, files in os.walk(directory):
...         for file in files:
...             # foo.txt → .txt
...             ext = os.path.splitext(file)[-1][1:]
...             if hasattr(visitor, ext):
...                 getattr(visitor, ext)(file)
...
>>> class FileReader(object):
...     def pdf(self, filename):
...         print('processing: {}'.format(filename))
...
>>> walker = visit('/Users/tarek/Desktop', FileReader())
processing slides.pdf
processing sholl23.pdf

If your application has data structures that are visited by more than one algorithm, the Visitor pattern will help in separating concerns. It is better for a data container to focus only on providing access to data and holding them, and nothing else.

Template

Template helps in designing a generic algorithm by defining abstract steps which are implemented in subclasses. This pattern uses the Liskov substitution principle, which is defined by Wikipedia as:

"If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program."

In other words, an abstract class can define how an algorithm works through steps that are implemented in concrete classes. The abstract class can also give a basic or partial implementation of the algorithm and let developers override its parts. For instance, some methods of the Queue class in the queue module can be overridden to make its behavior vary.

Let's implement an example, as shown in the figure that follows.

Template

Indexer is an indexer class that processes a text in five steps, which are common steps no matter what indexing technique is used:

  • Text normalization
  • Text split
  • Stop words removal
  • Stem words
  • Frequency

An Indexer provides partial implementation for the process algorithm but requires _remove_stop_words and _stem_words to be implemented in a subclass. BasicIndexer implements the strict minimum, while LocalIndex uses a stop word file and a stem words database. FastIndexer implements all steps and could be based on a fast indexer such as Xapian or Lucene.

A toy implementation can be:

from collections import Counter

class Indexer:
    def process(self, text):
        text = self._normalize_text(text)
        words = self._split_text(text)
        words = self._remove_stop_words(words)
        stemmed_words = self._stem_words(words)

        return self._frequency(stemmed_words)

    def _normalize_text(self, text):
        return text.lower().strip()

    def _split_text(self, text):
        return text.split()

    def _remove_stop_words(self, words):
        raise NotImplementedError

    def _stem_words(self, words):
        raise NotImplementedError

    def _frequency(self, words):
        return Counter(words)

From there, a BasicIndexer implementation can be:

class BasicIndexer(Indexer):
    _stop_words = {'he', 'she', 'is', 'and', 'or', 'the'}

    def _remove_stop_words(self, words):
        return (
            word for word in words
            if word not in self._stop_words
        )

    def _stem_words(self, words):
        return (
            (
                len(word) > 3 and
                word.rstrip('aeiouy') or
                word
            )
            for word in words
        )

And, like always, here is an example usage for the preceding example code:

>>> indexer = BasicIndexer()
>>> indexer.process("Just like Johnny Flynn said
The breath I've taken and the one I must to go on")
Counter({"i'v": 1, 'johnn': 1, 'breath': 1, 'to': 1, 'said': 1, 'go': 1, 'flynn': 1, 'taken': 1, 'on': 1, 'must': 1, 'just': 1, 'one': 1, 'i': 1, 'lik': 1})

Template should be considered for an algorithm that may vary and can be expressed into isolated substeps. This is probably the most used pattern in Python and does not always needs to be implemented via subclassing. For instance, a lot of built-in Python functions that deal with algorithmic problems accept arguments that allow you to delegate part of the implementation to external implementation. For instance, the sorted() function allows for an optional key keyword argument that is later used by a sorting algorithm. This is also the same for min() and max() functions that find minimum and maximum values in the given collection.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset