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

  • Enhancing the response of a component that is sending data to a second component
  • Supporting multiple optional behaviors

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

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

State Pattern

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

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

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

  • Connect to database
  • Construct a query
  • Issue the query
  • Parse the result
  • Output the data

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.