Blog Technology

Implementation of Common Design Patterns in Python

Roman Latyshenko

CTO

Design patterns in Python

In software development, design patterns are a proven solution to a common problem in a specific context. 

Their main goal is to show us good ways to program things and explain why other options won’t work. 

Using common design patterns, you can:

  • Speed up the development process;
  • Reduce the number of lines of code;
  • Make sure your code is well-designed;
  • Anticipate future problems arising from small issues.

Design patterns can considerably improve the life of a software developer disregarding the programming language (s)he uses. 

I mainly work with Python/ Django, so here is my list of top patterns in Python I use daily in my job.

Contents:

Behavioral patterns

Iterator

Iterator allows traversing the elements of collections without exposing the internal details. 

Use case. Mostly, I use it to provide a standard way of traversing the collections. 

Clean client code (Single Responsibility Principle).

Introducing iterators in collections is possible without changing the client’s code (Open/Closed Principle).

Each iteration object has its own iteration state, so you can delay & continue iteration.

Use of iterators with simple collections can overload the application.  

Structure

Iterator design pattern in Python

Code example

from __future__ import annotations
from collections.abc import Iterable, Iterator
from typing import Any, List


class AlphabeticalOrderIterator(Iterator):
    _position: int = None
    _reverse: bool = False

    def __init__(self, collection: WordsCollection, 
                 reverse: bool = False):
        self._collection = collection
        self._reverse = reverse
        self._position = -1 if reverse else 0

    def __next__(self):
        try:
            value = self._collection[self._position]
            self._position += -1 if self._reverse else 1
        except IndexError:
            raise StopIteration()
        return value


class WordsCollection(Iterable):
    def __init__(self, collection: List[Any] = []):
        self._collection = collection

    def __iter__(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection)

    def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection, True)

    def add_item(self, item: Any):
        self._collection.append(item)


if __name__ == "__main__":
    collection = WordsCollection()
    collection.add_item("First")
    collection.add_item("Second")
    collection.add_item("Third")

    print("Straight traversal:")
    print("\n".join(collection))

    print("Reverse traversal:")
    print("\n".join(collection.get_reverse_iterator()))

State

State helps an object to alter its behavior in case its internal state changes. 

Use case. State helps me

  • Alter the enormous number of object states. 
  • Reduce the number of lines with duplicate code in similar transitions & states.
  • Avoid massive conditionals.

Follows Single Responsibility principle: separate classes for the code related to a different state.

Doesn’t change the context or state of classes when adding new states (Open/Closed Principle).

Using State can be too much in case the state machine isn’t almost changing.

Structure

State design pattern in Python

Code example  

from __future__ import annotations
from abc import ABC, abstractmethod


class Context(ABC):
    _state = None

    def __init__(self, state: State):
        self.transition_to(state)

    def transition_to(self, state: State):
        print(f"Context: Transition to {type(state).__name__}")
        self._state = state
        self._state.context = self

    def request1(self):
        self._state.handle1()

    def request2(self):
        self._state.handle2()


class State(ABC):
    @property
    def context(self) -> Context:
        return self._context

    @context.setter
    def context(self, context: Context):
        self._context = context

    @abstractmethod
    def handle1(self):
        pass

    @abstractmethod
    def handle2(self):
        pass


class ConcreteStateA(State):
    def handle1(self):
        print("ConcreteStateA handles request1.")
        print("ConcreteStateA wants to change the state of the context.")
        self.context.transition_to(ConcreteStateB())

    def handle2(self):
        print("ConcreteStateA handles request2.")


class ConcreteStateB(State):
    def handle1(self):
        print("ConcreteStateB handles request1.")

    def handle2(self):
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the context.")
        self.context.transition_to(ConcreteStateA())


if __name__ == "__main__":
    context = Context(ConcreteStateA())
    context.request1()
    context.request2()

Observer

Observer notifies about events happening in other objects they observe without coupling to their classes. 

Use case. Each time I need to add the subscription mechanism to let an object subscribe to/ unsubscribe from notifications on the events happening with a specific publisher class, I use the Observer pattern. 

A good example is a simple subscription to news from any online magazine, frequently with the option to choose your sphere of interest (science, digital technology, etc.). Alternatively, the button “Notify me when it’s in stock” for e-commerce platforms is another example.

You haven’t to change the publisher’s code to add subscribers’ classes.

Subscribers get notifications in random order. 

Structure

Observer design pattern in Python

Code example

from __future__ import annotations
from abc import ABC, abstractmethod
from random import randrange
from typing import List


class Subject(ABC):
    @abstractmethod
    def attach(self, observer: Observer):
        pass

    @abstractmethod
    def detach(self, observer: Observer):
        pass

    @abstractmethod
    def notify(self):
        pass


class ConcreteSubject(Subject):
    _state: int = None
    _observers: List[Observer] = []
   
    def attach(self, observer: Observer):
        print("Subject: Attached an observer.")
        self._observers.append(observer)

    def detach(self, observer: Observer):
        self._observers.remove(observer)

    def notify(self):
        print("Subject: Notifying observers...")
        for observer in self._observers:
            observer.update(self)

    def some_business_logic(self):
        print("Subject: I'm doing something important.")
        self._state = randrange(0, 10)
        print(f"Subject: My state has just changed to: {self._state}")
        self.notify()


class Observer(ABC):
    @abstractmethod
    def update(self, subject: Subject):       
        pass


class ConcreteObserverA(Observer):
    def update(self, subject: Subject):
        if subject._state < 3:
            print("ConcreteObserverA: Reacted to the event")


class ConcreteObserverB(Observer):
    def update(self, subject: Subject):
        if subject._state == 0 or subject._state >= 2:
            print("ConcreteObserverB: Reacted to the event")


if __name__ == "__main__":    
    subject = ConcreteSubject()

    observer_a = ConcreteObserverA()
    subject.attach(observer_a)

    observer_b = ConcreteObserverB()
    subject.attach(observer_b)

    subject.some_business_logic()
    subject.some_business_logic()

    subject.detach(observer_a)

    subject.some_business_logic()

Structural patterns

Facade

Facade provides a simplified yet limited interface to decrease the complexity of an application. Complex subsystems with multiple moving parts could be “masked” by Facade. 

Use case. I create the Facade class in case I have to work with complex libraries & APIs and/ or I need only the part of their functionality. 

System complexity is separated from the code

Using the Facade pattern, you can create a god object. 

Structure

Facade design pattern in Python

Code example

class Addition:
    def __init__(self, field1: int, field2: int):
        self.field1 = field1
        self.field2 = field2

    def get_result(self):
        return self.field1 + self.field2


class Multiplication:
    def __init__(self, field1: int, field2: int):
        self.field1 = field1
        self.field2 = field2

    def get_result(self):
        return self.field1 * self.field2


class Subtraction:
    def __init__(self, field1: int, field2: int):
        self.field1 = field1
        self.field2 = field2

    def get_result(self):
        return self.field1 - self.field2


class Facade:
    @staticmethod
    def make_addition(*args) -> Addition:
        return Addition(*args)

    @staticmethod
    def make_multiplication(*args) -> Multiplication:
        return Multiplication(*args)

    @staticmethod
    def make_subtraction(*args) -> Subtraction:
        return Subtraction(*args)


if __name__ == "__main__":
    addition_obj = Facade.make_addition(5, 5)
    multiplication_obj = Facade.make_multiplication(5, 2)
    subtraction_obj = Facade.make_subtraction(15, 5)

    print(addition_obj.get_result())
    print(multiplication_obj.get_result())
    print(subtraction_obj.get_result())

Decorator

Decorator attaches new behaviors to the objects without modifying their structure. 

The pattern produces a decorator class to wrap the original one and add new functionality.  

Use case. I use the Decorator pattern each time I need to add extra behaviors to objects without getting into the code. 

Changes the object behavior without creating a subclass.

➕ You can combine several behaviors by wrapping an object into multiple decorators.

A specific decorator is hard to remove from the wrappers stack. 

Structure

Decorator design pattern in Python

Code example 

class my_decorator:
    def __init__(self, func):
        print("inside my_decorator.__init__()")
        func() # Prove that function definition has completed

    def __call__(self):
        print("inside my_decorator.__call__()")


@my_decorator
def my_function():
    print("inside my_function()")


if __name__ == "__main__":    
    my_function()

Adapter

Adapter serves as the middle-layer class to join functionalities of either independent or incompatible interfaces.

Use case. Setting up the collaboration between the interfaces, I use the Adapter pattern to resolve the problem of incompatible formats. 

For example, Adapter could help convert XML data format to JSON for further analysis. 

Allows separating the interface from business logic.

Adding new adapters doesn’t break the client’s code

Increases the code complexity

Structure

Adapter design pattern in Python

Code example

class Target:
    def request(self):
        return "Target: The default target's behavior."


class Adaptee:
    def specific_request(self):
        return ".eetpadA eht fo roivaheb laicepS"


class Adapter(Target, Adaptee):
    def request(self):
        return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"


def client_code(target: "Target"):
    print(target.request())


if __name__ == "__main__":
    print("Client: I can work just fine with the Target objects:")

    target = Target()
    client_code(target)

    adaptee = Adaptee()
    
    print("Client: The Adaptee class has a weird interface. "
          "See, I don't understand it:")
    print(f"Adaptee: {adaptee.specific_request()}")

    print("Client: But I can work with it via the Adapter:")
    
    adapter = Adapter()
    client_code(adapter)

Creational patterns

Singleton

Singleton restricts a class from having more than one instance and ensures a global access point to this instance. 

Use case. Singleton helps me

  • Manage a shared resource: i.e. a single database, file manager, or printer spooler shared by multiple parts of the application. 
  • Store a global state (help filepath, user language, application path, etc.).
  • Create a simple logger.

Class has a single instance

Violates the SRP (Single Responsibility Principle). 

➖ It’s hard to unit test the code as the majority of test frameworks use inheritance when creating mock objects. 

Structure

Singleton design pattern in Python

Code example

class Singleton:
    def __new__(cls):
        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls).__new__(cls)
        return cls.instance


if __name__ == "__main__":
    s = Singleton()
    print("Object created:", s)

    s1 = Singleton()
    print("Object created:", s1)

When to use design patterns for Python?

Facade is good when you need a unified interface for several API options. I.e. you should integrate a payment system in the application, leaving the possibility to change it. In this case, you could use the Facade pattern, and you’ll only have to create a new facade without rewriting the whole application.

The problem here can emerge if only APIs are quite different, as it isn’t an easy task to design the common interface for facades.  

State is used to manage the multiple independent components of an application provided that the initial architecture implies their independence. So, creating a separate module for state management can be a good idea, as well as using the Observer pattern. 

Decorator is probably the most used Python pattern, because of in-built decorator support. For example, Decorator provides a convenient and explicit way to use some libraries and creates ever-richer opportunities for application design & management. The pattern also ensures a wide possibility for function composition and discovers new opportunities in functional programming. 

Adapter is a fit when working with a big amount of data in different formats. The pattern allows using one algorithm instead of multiple ones for every data format. 

The similar benefits Iterator has, so they are okay to be used together. In addition, one of the Iterator variations called Generator (introduced in Python long ago) allows using memory more efficiently working with big amounts of data that is highly valuable for some types of projects. 

Finally, the importance of Singleton can’t be underestimated: database connection, working with API, working with files… These all are the moments when a developer should have a clear idea of how the process goes to avoid making mistakes. And Singleton can do a good job here, not to mention the possibility to reduce the memory consumption by using the same instance each time instead of duplicating it. 

    Subscribe to Our Newsletter

    No spam, only hot&fresh posts from Jellyfish.tech team

    Got anything to add? Welcome to comments, господа!

    Leave a Reply

    Your email address will not be published. Required fields are marked *