3 Top Design Patterns in Python: Singletons, Decorators, and Iterators

Rate this post

Ok, I must admit at the beginning that this topic is a bit of a clickbait – but if you find it cheating, I have to write in my defense that it was in good faith.

If you were starting to write a book, it wouldn’t cross your mind to ask “what are the top plot elements that I should learn to be able to create an interesting story?” because you need as much context and life experience as you can assemble.

Gangs of Four

The book “Design Patterns: Elements of Reusable Object-Oriented Software” (by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides), thanks to which design patterns gained popularity in computer science, isn’t about telling you the best ways to do things.

It’s about teaching your brain to pick up patterns that can be applied to existing code — to give you the highest leverage as a developer.

It’s a huge toolbox of tools and some of them are used more often than others, but the fact that a tool is frequently used does not mean that you should use it for all your work.

Instead, you should learn as many patterns as possible – to be able to choose the right one when you notice the possibility of its use.

The book Gangs of Four (that’s what it is called in the industry) is primarily about patterns for Java, and to a lesser extent for C++, but here we are writing in a different language, Python, so in this short article I chose a few design patterns from each category (according to the originally proposed classification) that I found interesting in the context of Python programming.

I sincerely hope that it will inspire you to learn more about this issue on your own, and who knows, maybe there will be more similar articles on the Finxter website in the future.

What’s a Software Design Pattern?

In software design a design pattern is a general, reusable solution to a commonly occurring problem within a given context.

They are like pre-made blueprints that you can customize to solve a problem in your code.

It is not possible to apply a design pattern just like you would use a function from a newly imported library (the pattern is not a code snippet, but a general concept that describes how to solve a specific recurring problem).

Instead, you should follow the pattern details and implement a solution that suits the requirements of your program.

Classification of Design Patterns

Initially, there were two basic classifications of design patterns – based on what problem the pattern solves, and based on whether the pattern concerns classes or objects. Taking into account the first classification, the patterns can be divided into three groups:

  1. Creational – provide the capability to create, initialize and configure objects, classes, and data types based on a required criterion and in a controlled way.
  2. Structural – help to organize structures of related objects and classes, providing new functionalities.
  3. Behavioral – are about identifying common communication patterns between objects.

Later, new design patterns appeared, from which another category can be distinguished:

  1. Concurrency – those types of design patterns that deal with the multi-threaded programming paradigm.

Pattern 1: Singleton

The Singleton is a creational pattern the purpose of which is to limit the possibility of creating objects of a given class to one instance and to ensure global access to the created object.

Use Cases

  • A class in your program has only a single instance available to all clients such as a single database object shared by different parts of the program.
  • You need stricter control over global variables.

Code examples

First naive approach

class Logger:
   @staticmethod
   def get_instance():
       if '_instance' not in Logger.__dict__:
           Logger._instance = Logger()
       return Logger._instance

   def write_log(self, path):
       pass


if __name__ == "__main__":
   s1 = Logger.get_instance()
   s2 = Logger.get_instance()
   assert s1 is s2

What is wrong with this code?

It violates the single responsibility principle and has non-standard class access (you must remember to access instances of the class only by the get_instance() method) – we try to fix these problems in another code example.

class Singleton:
   _instances = {}

   def __new__(cls, *args, **kwargs):
       if cls not in cls._instances:
           instance = super().__new__(cls)
           cls._instances[cls] = instance
       return cls._instances[cls]


class Logger(Singleton):
   def write_log(self, path):
       pass


if __name__ == "__main__":
   logger1 = Logger()
   logger2 = Logger()
   assert logger1 is logger2

So the problems from the previous example have been addressed, but can we take a better approach (without inheritance)?

Let’s try.

class Singleton(type):
   _instances = {}

   def __call__(cls, *args, **kwargs):
       if cls not in cls._instances:
           instance = super().__call__(*args, **kwargs)
           cls._instances[cls] = instance
       return cls._instances[cls]


class Logger(metaclass=Singleton):
   def write_log(self, path):
       pass


if __name__ == "__main__":
   logger1 = Logger()
   logger2 = Logger()
   assert logger1 is logger2

Great, it works, but we should make one more adjustment – prepare our program to run in a multi-threaded environment.

from threading import Lock, Thread


class Singleton(type):
   _instances = {}
   _lock: Lock = Lock()

   def __call__(cls, *args, **kwargs):
       with cls._lock:
           if cls not in cls._instances:
               instance = super().__call__(*args, **kwargs)
               cls._instances[cls] = instance
       return cls._instances[cls]


class Logger(metaclass=Singleton):
   def __init__(self, name):
       self.name = name

   def write_log(self, path):
       pass


def test_logger(name):
   logger = Logger(name)
   print(logger.name)


if __name__ == "__main__":
   process1 = Thread(target=test_logger, args=("FOO",))
   process2 = Thread(target=test_logger, args=("BAR",))
   process1.start()
   process2.start()

Output:

FOO
FOO

Both processes called constructors with two different parameters, but only one instance of the Logger class was created – our hard work is finally over!

Consequences

  • You know that a class has only a single instance;
  • You gain a global access point to that instance;
  • The singleton is initialized only when requested for the first time;
  • Masks bad design to a certain point. For example, when the components of the program know too much about each other. Consequently, many consider it as an anti-pattern.

Sources

  • Dive Into Design Patterns by Alexander Shvets
  • Python Design Patterns Playbook by Gerald Britton (from Pluralsight)

Pattern 2: Decorator

The Decorator is a structural pattern the purpose of which is to provide new functionalities to classes/objects at runtime (unlike inheritance, which allows you to achieve a similar effect, but at the compilation time).

The decorator is most often an abstract class that takes an object in the constructor, the functionality of which we want to extend — but in Python, there is also a built-in decorator mechanism that we can use.

Use Cases

  • You want to assign additional responsibilities to objects at runtime without breaking the code using these objects;
  • You cannot extend an object’s responsibilities through inheritance for some reason.

Code examples

Using decorators, you can wrap objects multiple times because both the target and the decorators implement the same interface.

The resulting object will have the combined and stacked functionality of all wrappers.

from abc import ABC, abstractmethod


class Component(ABC):
   @abstractmethod
   def operation(self):
       pass


class ConcreteComponent(Component):
   def operation(self):
       return "ConcreteComponent"


class Decorator(Component):
   def __init__(self, component):
       self.component = component

   @abstractmethod
   def operation(self):
       pass


class ConcreteDecoratorA(Decorator):
   def operation(self):
       return f"ConcreteDecoratorA({self.component.operation()})"


class ConcreteDecoratorB(Decorator):
   def operation(self):
       return f"ConcreteDecoratorB({self.component.operation()})"


if __name__ == "__main__":
   concreteComponent = ConcreteComponent()
   print(concreteComponent.operation())
   decoratorA = ConcreteDecoratorA(concreteComponent)
   decoratorB = ConcreteDecoratorB(decoratorA)
   print(decoratorB.operation())

Output:

ConcreteComponent
ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))

And a slightly more practical example using the built-in decorator mechanism.

import sys


def memoize(f):
   cache = dict()

   def wrapper(x):
       if x not in cache:
           cache[x] = f(x)
       return cache[x]

   return wrapper


@memoize
def fib(n):
   if n <= 1:
       return n
   else:
       return fib(n - 1) + fib(n - 2)


if __name__ == "__main__":
   sys.setrecursionlimit(2000)
   print(fib(750))

Output:

2461757021582324272166248155313036893697139996697461509576233211000055607912198979704988704446425834042795269603588522245550271050495783935904220352228801000

Without using the cache decorator for the function (that recursively computes the n-th term of the Fibonacci series), we probably would not have computed a result for value 100 in our lifetime.

Consequences

  • Extend the behavior of an object without creating a subclass;
  • Add or remove object responsibilities at runtime;
  • Combine multiple behaviors by applying multiple decorators to an object;
  • Divide a monolithic class that implements many variants of behavior into smaller classes;
  • It’s difficult to take one particular wrapper from the center of the wrappers stack;
  • It’s difficult to implement a decorator in such a way that its behavior does not depend on the order in which the wrapper is stacked.

Sources

  • Dive Into Design Patterns by Alexander Shvets
  • Python. Kurs video. Wzorce czynnoΕ›ciowe i architektoniczne oraz antywzorce byΒ  Karol Kurek

Pattern 3: Iterator

Iterator is a behavioral pattern the purpose of which is to allow you to traverse elements of a collection without exposing its underlying representation.

To implement your iterator in Python, we have two possible options:

Use Cases

  • The collection has a complicated structure and you want to hide it from the client for convenience or security reasons;
  • You want to reduce duplication of the traversal code across your app;
  • You want your code to be able to traverse elements of different data structures or when you don’t know the details of their structure in advance.

Code Examples

In the example below, we will see how we can create a custom collection with an alphabetical order iterator.

from collections.abc import Iterator, Iterable


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

   def __init__(self, collection, reverse=False):
       self._collection = sorted(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):
       self._collection = collection

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

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


if __name__ == "__main__":
   wordsCollection = WordsCollection(["Third", "First", "Second"])
   print(list(wordsCollection))
   print(list(wordsCollection.get_reverse_iterator()))

Output:

['First', 'Second', 'Third']
['Third', 'Second', 'First']

The next example is for a generator, which is a special kind of function that can be paused and resumed from where it was paused.

Based on the stored state, it is possible to return different values during subsequent calls of the generator.

def prime_generator():
   yield 2
   primes = [2]
   to_check = 3
   while True:
       sqrt = to_check ** 0.5
       is_prime = True
       for prime in primes:
           if prime > sqrt:
               break
           if to_check % prime == 0:
               is_prime = False
               break
       if is_prime:
           primes.append(to_check)
           yield to_check
       to_check += 2


generator = prime_generator()
print([next(generator) for _ in range(20)])

Output:

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]

Consequences

  • You can clean up the client code and collections by extracting the traversal code into separate classes;
  • You can implement new collection types and iterators and pass them into existing code without breaking anything;
  • You can iterate the same collection with multiple iterators in parallel because each of them stores information about its iteration state;
  • For this reason, you can delay the iteration and continue it as needed;
  • The use of this pattern will be overkill if your application only works with simple collections;
  • Using an iterator may be less efficient than traversing directly through the items of some specialized collection.

Sources

  • Dive Into Design Patterns by Alexander Shvets
  • Python. Kurs video. Kreacyjne i strukturalne wzorce projektowe byΒ  Karol Kurek

Conclusion

The bottom line is even if you never encounter problems that are solved by the design patterns mentioned in the article, knowing patterns is still useful because it teaches you to solve problems using principles of object-oriented design.