alecor.net

Search the site:

2020-04-11

Design patterns in Python

Summary:

Quick, concise introduction on implementation of design patterns in Python, with example code.

Python Design Patterns

What Is A Design Pattern?

Everything starts with the Gang of Four (GOF).

Design patterns are a common way of solving well known problems. Two main principles are in the bases of the design patterns defined by the GOF:

  1. Program to an interface not an implementation.
  2. Favor object composition over inheritance.

Let's take a closer look at these two principles from the perspective of Python programmers.

  1. Program to an interface not an implementation

Think about Duck Typing. In Python we don't like to define interfaces and program classes according these interfaces, but this doesn't mean we don't think about interfaces, in fact with Duck Typing we do that all the time.

The Duck Typing approach to see how it fits in this paradigm: program to an interface.

If it looks like a duck and quacks like a duck, it's a duck.

Don't bother with the nature of the object, we don't have to care what the object is; we just want to know if it's able to do what we need (we are only interested in the interface of the object).

If the object can quack, let it quack.

try:
    bird.quack()
except AttributeError:
    self.lol()

The interface for our duck was not defined, we designed the interface instead of the implementation.

As Alex Martelli points out in his well known presentation about Design Patterns in Python, "Teaching the ducks to type takes a while, but saves you a lot of work afterwards!"

  1. Favor object composition over inheritance

That's a Pythonic principle; instead of doing this:

class User(DbObject):
    pass

We can do something like this:

class User:
    _persist_methods = ['get', 'save', 'delete']

    def __init__(self, persister):
        self._persister = persister

    def __getattr__(self, attribute):
        if attribute in self._persist_methods:
            return getattr(self._persister, attribute)

The advantages are clear. We can restrict what methods of the wrapped class to expose. We can inject the persister instance in runtime, for example, today it's a relational database, but tomorrow it could be anything else, with the interface we need.

Composition is elegant and natural to Python.

Behavioral Patterns

Behavioural Patterns involve communication between objects, how objects interact and fulfil a given task. According to GOF principles, there are a total of 11 behavioral patterns in Python: Chain of responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template and Visitor.

Behavioural patterns deal with inter-object communication, controlling how various objects interact and perform different tasks.

Iterator

Iterators are built into Python. This is one of the most powerful characteristics of the language. Learn enough about Python iterators and generators and you'll know everything you need about this particular Python pattern.

Chain of responsibility

This pattern gives us a way to treat a request using different methods, each one addressing a specific part of the request. You know, one of the best principles for good code is the Single Responsibility Principle.

Every piece of code must do one, and only one, thing. This principle is deeply integrated in this design pattern.

For example, if we want to filter some content we can implement different filters, each one doing one precise and clearly defined type of filtering. These filters could be used to filter offensive words, ads, unsuitable video content, and so on.

class ContentFilter(object):
    def __init__(self, filters=None):
        self._filters = list()
        if filters is not None:
            self._filters += filters

    def filter_(self, content):
        for filter_ in self._filters:
            content = filter_(content)
        return content

filter_ = ContentFilter([
            offensive_filter,
            ads_filter,
            porno_video_filter
        ])
filtered_content = filter.filter_(content)

Command

"Patterns are not invented, they are discovered". They exist, we just need to find and put them to use.

The command pattern is handy in situations when we need to start by preparing what will be executed and then to execute it when needed. The advantage is that encapsulating actions in such a way enables Python developers to add additional functionalities related to the executed actions, such as undo/redo, or keeping a history.

Let's see what a simple and frequently used example looks like:

class RenameFileCommand(object):
    def __init__(self, from_name, to_name):
        self._from = from_name
        self._to = to_name

    def execute(self):
        os.rename(self._from, self._to)

    def undo(self):
        os.rename(self._to, self._from)

class History(object):
    def __init__(self):
        self._commands = list()

    def execute(self, command):
        self._commands.append(command)
        command.execute()

    def undo(self):
        self._commands.pop().undo()

history = History()
history.execute(RenameFileCommand('docs/cv.doc', 'docs/cv-en.doc'))
history.execute(RenameFileCommand('docs/cv1.doc', 'docs/cv-bg.doc'))
history.undo()
history.undo()

Creational Patterns

Creational patterns are not commonly used in Python because of the dynamic nature of the language.

Someone once said that Factory is built into Python. It means that the language itself provides us with all the flexibility we need to create objects in a sufficiently elegant fashion; we rarely need to implement anything on top, like Singleton or Factory.

One definition of creational design patterns stated these "design patterns provide a way to create objects while hiding the creation logic, rather than instantiating objects directly using a new operator."

That pretty much sums up the problem: We don't have a new operator in Python in the same manner as in other languages.

Nevertheless, let's see how we can implement a few, should we feel we might gain an advantage by using such patterns.

Singleton

The Singleton pattern is used when we want to guarantee that only one instance of a given class exists during runtime. Do we really need this pattern in Python? Based on my experience, it's easier to simply create one instance intentionally and then use it instead of implementing the Singleton pattern.

But should you want to implement it, here is some good news: In Python, we can alter the instantiation process (along with virtually anything else).

class Logger(object):
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_logger'):
            cls._logger = super(Logger, cls).__new__(cls, *args, **kwargs)
        return cls._logger

In this example, Logger is a Singleton.

These are the alternatives to using a Singleton in Python:

  • Use a module.

Create one instance somewhere at the top-level of your application, perhaps in the config file.

Pass the instance to every object that needs it. That's a dependency injection and it's a powerful and easily mastered mechanism.

  • Dependency Injection

Whether dependency injection is a design pattern is a different discussion, but it's a very good mechanism of implementing loose couplings, and it helps make our application maintainable and extendable. Combine it with Duck Typing and the Force will be with you. Always.

Duck? Human? Python does not care. It's flexible.

It is listed in the creational pattern section here because it deals with the question of when (or even better: where) the object is created. It's created outside. Better to say that the objects are not created at all where we use them, so the dependency is not created where it is consumed. The consumer code receives the externally created object and uses it.

It's a nice explanation of dependency injection and gives us a good idea of the potential of this particular technique. Basically the answer explains the problem with the following example: Don't get things to drink from the fridge yourself, state a need instead. Tell your parents that you need something to drink with lunch.

Python offers us all we need to implement that easily. Think about its possible implementation in other languages such as Java and C#, and you'll quickly realize the beauty of Python.

Let's think about a simple example of dependency injection:

class Command:
    def __init__(self, authenticate=None, authorize=None):
        self.authenticate = authenticate or self._not_authenticated
        self.authorize = authorize or self._not_autorized

    def execute(self, user, action):
        self.authenticate(user)
        self.authorize(user, action)
        return action()

if in_sudo_mode:
    command = Command(always_authenticated, always_authorized)
else:
    command = Command(config.authenticate, config.authorize)

command.execute(current_user, delete_user_action)

We inject the authenticator and authorizer methods in the Command class. All the Command class needs is to execute them successfully without bothering with the implementation details. This way, we may use the Command class with whatever authentication and authorization mechanisms we decide to use at runtime.

We have shown how to inject dependencies through the constructor, but we can easily inject them by setting directly the object properties, unlocking even more potential:

command = Command()

if in_sudo_mode:
    command.authenticate = always_authenticated
    command.authorize = always_authorized
else:
    command.authenticate = config.authenticate
    command.authorize = config.authorize

command.execute(current_user, delete_user_action)

There is much more to learn about dependency injection; curious people would search for IoC, for example.

Again, we just demonstrated how implementing this wonderful design pattern in Python is just a matter of using the built-in functionalities of the language.

Let's not forget what all this means: The dependency injection technique allows for very flexible and easy unit-testing. Imagine an architecture where you can change data storing on-the-fly. For example, mocking a database becomes a trivial task.

You may also want to research Prototype, Builder and Factory design patterns.

Structural Patterns

Facade

This may very well be the most famous Python design pattern.

Imagine you have a system with a considerable number of objects. Every object is offering a rich set of API methods. You can do a lot of things with this system, but how about simplifying the interface? Why not add an interface object exposing a well thought-out subset of all API methods? A Facade!

Facade is an elegant Python design pattern. It's a perfect way of streamlining the interface.

Python Facade design pattern example:

class Car(object):
    def __init__(self):
        self._tyres = [Tyre('front_left'),
                       Tyre('front_right'),
                       Tyre('rear_left'),
                       Tyre('rear_right'),]
        self._tank = Tank(70)

    def tyres_pressure(self):
        return [tyre.pressure for tyre in self._tyres]

    def fuel_level(self):
        return self._tank.level

There is no surprise, no tricks, the Car class is a Facade, and that's all.

Adapter

If Facades are used for interface simplification, Adapters are all about altering the interface. Like using a cow when the system is expecting a duck.

Let's say you have a working method for logging information to a given destination. Your method expects the destination to have a write() method (as every file object has, for example).

def log(message, destination):
    destination.write('[{}] - {}'.format(datetime.now(), message))

It is a well written method with dependency injection, which allows for great extensibility. Say you want to log to some UDP socket instead to a file, you know how to open this UDP socket, but the only problem is that the socket object has no write() method. You need an Adapter!

import socket

class SocketWriter(object):
    def __init__(self, ip, port):
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self._ip = ip
        self._port = port

    def write(self, message):
        self._socket.send(message, (self._ip, self._port))

def log(message, destination):
    destination.write('[{}] - {}'.format(datetime.now(), message))

upd_logger = SocketWriter('1.2.3.4', '9999')
log('Something happened', udp_destination)

When the adapter pattern it's effectively combined with dependency injection, it gives us huge flexibility. Why alter our well-tested code to support new interfaces when we can just implement an adapter that will translate the new interface to the well known one?

You should also check out and master bridge and proxy design patterns, due to their similarity to adapter. Think how easy they are to implement in Python, and think about different ways you could use them in your project.

Decorator

Decorators are really nice, and we already have them integrated into the language. Python simplifies using best practices. It's not that we don't have to be conscious about best practices (and design patterns, in particular), most developers find that in Python best practices are intuitive and second nature, and this is something appreciated by novice and elite developers alike.

The decorator pattern is about introducing additional functionality and in particular, doing it without using inheritance.

So, let's check out how we decorate a method without using built-in Python functionality. Here is a straightforward example:

def execute(user, action):
    self.authenticate(user)
    self.authorize(user, action)
    return action()

What is not so good here is that the execute function does much more than executing something. We are not following the single responsibility principle to the letter.

It would be good to simply:

def execute(action):
    return action()

We can implement any authorization and authentication functionality in another place, in a decorator, like so:

def execute(action, *args, **kwargs):
    return action()

def autheticated_only(method):
    def decorated(*args, **kwargs):
        if check_authenticated(kwargs['user']):
            return method(*args, **kwargs)
        else:
            raise UnauthenticatedError
        return decorated

def authorized_only(method):
    def decorated(*args, **kwargs):
        if check_authorized(kwargs['user'], kwargs['action']):
            return method(*args, **kwargs)
        else:
            raise UnauthorizeddError
    return decorated

execute = authenticated_only(execute)
execute = authorized_only(execute)

Now the execute() method is:

  • Simple to read
  • Does only one thing (at least when looking at the code)
  • Is decorated with authentication
  • Is decorated with authorization

We write the same using Python's integrated decorator syntax:

def autheticated_only(method):
    def decorated(*args, **kwargs):
        if check_authenticated(kwargs['user']):
            return method(*args, **kwargs )
        else:
            raise UnauthenticatedError
    return decorated

def authorized_only(method):
    def decorated(*args, **kwargs):
        if check_authorized(kwargs['user'], kwargs['action']):
            return method(*args, **kwargs)
        else:
            raise UnauthorizedError
    return decorated

@authorized_only
@authenticated_only
def execute(action, *args, **kwargs):
    return action()

Python is not limited to functions as decorators. The only requirement is that they must be callables. In Python, it is only necessary to define the __call__(self) method in a class to make it callable.

Conclusion

Python makes it easy to implement and think about design patterns. Some are intuitive, and others are already embedded and simplified within the language, both syntactically and conceptually.

Nothing you read here should be considered advice or recommendation. Everything is purely and solely for informational purposes.