← Back

Pythonic Design Patterns

PythonDesign PatternsBig O NotationCore Collections
Published:


Big O Notation

Describes the time taken to complete an operation. It is done to indicate the approximate performance of an operation based one the number of steps that need to be executed.

When we say a function takes O(n) time, it means that it would take x number of seconds for n operation steps. Where n is the size or length of the object.

Core Collections

List

Operations of list includes append, get, set and len function takes O(1) time - the best possible. Operations such as remove, insert have O(n) which is the worst-case time complexity.

To delete a single item out of 1,000 items, Python might have to walk through 1,000 items (the worst case).

Dict

The average time complexity is O(1) for the get, set, and delete operations. Deleting a item will result in both copying and iterating over the entire dictionary (which takes O(m) where m is the max size of the dict).

Dict works by converting the key into a hash using the hash function (__hash__ method)

Set

Uses the hash to get a unique collection of values. One way a set is useful is to calculate the differences between two objects.

Tuple

They are similar to a list but is immutable. It has the hash function where you can use a tuple as a key in a dict or an item of a set which a list can’t do.

Can unpack variables

spam, *eggs = 1, 2, 3, 4
print(spam) # 1
print(eggs) # [2, 3, 4]

Extensions of Core Collections

Setting data storage with Type Hinting

Uses the type hinting system which was introduced in Python 3.5 to automatically generate classes - including documentations and constructors based on the types.

Available since Python 3.7 using of the dataclasses module.

import dataclasses

@dataclasses.dataclass
class Sandwich:
	spam: int
	eggs: int = 3

print(sandwich := Sandwich(1, 2)) # Sandwich(spam=1, eggs=2)
print(dataclasses.asdict(sandwich)) # {'spam': 1, 'eggs': 2}
print(dataclasses.astuple(sandwich)) # (1, 2)

# To view more flags for dataclass
# e.g. Frozen flag, makes everything read only
print(help(dataclasses.dataclass))

Example Example 1

Combining multiple scopes with ChainMap

ChainMap that was introduced in Python 3.3, allows you to combine multiple mappings (e.g. dict) into one. It can include multiple keys and it will choose the first key it finds.

import collections

mappings = collections,ChainMap(
	locals(), globals(), vars(builtins)
)
mappings[key]

Useful for reading configurations from multiple sources and simply getting the first matching item.

Default Dictionary values using defaultdict

A useful tool where when there’s no key value of your choosing, it will create a new item with that key and value you’ve provided.

import collections

nodes = [
	['a,', 'b'],
	['a', 'c'],
	['b', 'a']
	# ...
]

graph = collections.defaultdict(list)
for from_, to in nodes:
	graph[from_].append(to)

print(graph)

tree Example

import json
import collections

def tree():
	return collections.defaultdict(tree)

colors = tree()
# Can go as deep as you want
colors['other']['black'] = Ox000000
colors['primary']['green'] = Ox00FF00

print(json.dumps(colors, sort_keys=True, indent=4))

Python enums

Introduced in Python 3.4 where you can create reusable constants for your module.

import enum

class Color(enum.Enum):
	red = 1
	green = 2
	blue = 3

print(Color.red)

Makes cleaner validation than having to use if/else several times.

Creating a priority Queue

It is a data structure that will always make the smallest (or largest) item available with minimum effort.

import heapq

heap = [1, 3, 5, 7 ,2 , 4, 3]
heapq.heapify(heap)

while heap:
	print(heapq.heappop(heap), heap)

If you’re looking for a structure to keep your list always sorted, use bisect.

Searching through sorted Collection

Using bisect, it inserts items in an object in such a way that they stay sorted and are easily searchable. Use this is you’re primary purpose is to search else, use heapq.

Global Instances using Borg or Singleton patterns

Singleton pattern ensures that only a single instance of a class will ever exist. Another alternative is the Borg pattern where it enforces a single state for all instances and subclass as well.

Borg Class

class Borg:
	_state = {}
	def __init__(self):
		self.__dict__ = self._state

class SubBorg(Borg):
	pass

a = Borg()
b = Borg()
a.a_property = 123
print(b.a_property) # 123

Singleton Class

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

class SubSingleton(Singleton):
	pass

a = Singleton()
b = Singleton()
a.a_property = 123
print(b.a_property) # 123

Getters and Setters with Class Properties

class Sandwich:
	def __init__(self, spam):
		self._spam = spam

	# This will make this a read-only property, to make it read-write, include the setter
	@property
	def spam(self):
		return self._spam

	@spam.setter
	def spam(self, value):
		self._spam = value
		if self._spam >= 5:
			print("you're hungry")

	@spam.deleter
	def spam(self):
		self._spam = 0

sandwich = Sandwich(2)
sandwich.spam += 1
sndwich.spam += 2

del sandwich.spam
print(sandwich.spam) # 0

Dict union operators

Used to easily combine multiple dict instances together. If there is a duplicate key, the value from the right-hand side dictionary takes precedence.

a = dict(x=1,y=2)
b = dict(y=1, z=2)

a | b # {'x':1, 'y':1, 'z': 2}