Python Design Patterns, Part One

In this article, I will cover several common Python design patterns.

Design Patterns

Design patterns are an attempt to bring a formal definition for correctly designed structures to software engineering. In software development, we may incorrectly choose or apply a design pattern, and create software that “collapses” under normal operating situations or when stressed beyond its original design limits.

Decorator Pattern

Decorators allows us to “wrap” and object that provides core functionality with other objects that alter that functionality. There are two primary uses of the decorator pattern:

Example:

import time


def log_calls(func):
# Defines a new function
def wrapper(*args, **kwargs):
now = time.time()
print(f"Calling {func.__name__} with {args} and {kwargs}")
return_value = func(*args, **kwargs)
print(f"Executed {func.__name__} in {time.time() - now}ms")
return return_value
# Return the new function
return wrapper


@log_calls
def test1(a, b, c):
print(f"\t test1 called")


@log_calls
def test2(a, b):
print(f"\t test2 called")


@log_calls
def test3(a, b):
print(f"\t test1 called")
time.sleep(1)


test1(1, 2, 3)
test2(4, b=5)
test3(6, 7)

Observer Pattern

The observer pattern is useful for state monitoring and event handling situations. This pattern ensures a single core object can be monitored by an unknown, possibly expanding array of “observer” objects. Whenever a value on the core object changes, it lets all the observer object know that a change has occurred, by calling a update() method. Each observer maybe responsible for different tasks whenever the core object changes.

Example:

class Inventory:
def __init__(self):
self.observers = []
self._product = None
self._quantity = 0

def attach(self, observer):
self.observers.append(observer)

@property
def product(self):
return self._product

@product.setter
def product(self, value):
self._product = value
self._update_observers()

@property
def quantity(self):
return self._quantity

@quantity.setter
def quantity(self, value):
self._quantity = value
self._update_observers()

def _update_observers(self):
for observer in self.observers:
observer()


class ConsoleObserver:
def __init__(self, inventory):
self.inventory = inventory

def __call__(self):
if self.inventory.quantity > 4:
print(f"{self.inventory.product} has {self.inventory.quantity}, overload warning!")
print(self.inventory.product)
print(self.inventory.quantity)

The observer pattern detaches the code being monitored from the code doing the monitoring. Otherwise we would have had to put code in each of the properties to handle the different cases that might come up, logging to a file, update a database,..etc.

Strategy Pattern

Strategy pattern is a common demonstration of abstraction in OO. The pattern implements different solutions to a single problem each in a different object. The client code can then choose the most appropriate implementation dynamically at runtime. A very typical usage of the strategy pattern is the sorting. We have numerous algorithms have been invented for sorting: quick sort, merge sort, bubble sort, heap sort,..etc. If we have client code that needs to sort a collection, we could pass it to an object with sort() method. This object may be a QuickSorter or a MergeSorter object, but the result will be the same: a sorted collection. The strategy used to do the sorting is abstracted from the calling code, making it modular and replaceable.

State Pattern

State pattern is structurally similar to strategy pattern, but its goal is different. The goal of the state pattern is to represent state-transition systems: systems where it is obvious that an object can be in a specific state, and that certain activities may drive it to a different state.

To make state pattern work, we need a context manager that provides an interface for switching states. Internally, the manager contains a pointer to the current state; each state knows what other states it is allowed to be in and will transition to those states depending on actions invoked upon it. An XML parsing tool is a good example. The tool keeps eating eating characters from the XML file, looking for a specific value and change to its different state.

Singleton Pattern

Singleton pattern is one of the most famous of all design patterns. It is very useful in overly OO languages, and is a vital part of OO programming. The basic idea behind the singleton pattern is to allow exactly one instance of a certain object to exist. Such object often needs to be referenced by a wide variety of other objects, and passing references to the manager object around to the methods and constructors that need them can make code hard to read.

class OneOnly:
_singleton = None

def __new__(cls, *args, **kwargs):
if not cls._singleton:
cls._singleton = super(OneOnly, cls).__new__(cls, *args, **kwargs)
return cls._singleton

Template Pattern

Template pattern is useful for removing duplicate code. It is designed for situations where we have several different tasks to accomplish that have some, but not all, steps in common. The common steps are implemented in a base class, and the different steps are overridden in subclasses to provide custom behavior.

A typical example is database operations, when doing database operations, we need to perform the following common steps:

We could create a database template class as follows:

class QueryTemplate:
def connect(self):
pass
def construct_query(self):
pass
def do_query(self):
pass
def parse_results(self):
pass
def output_results(self):
pass
def process_query(self):
self.connect()
self.construct_query()
self.do_query()
self.parse_results()
self.output_results()
class NewUserQuery(QueryTemplate):
def construct_query(self):
self.query = "select * from Users where new='true'"

Senior Cloud and DevOps Engineer from both HighTech and FinTech world.