Pythonic Design Patterns
- Big O Notation
- Core Collections
- Extensions of Core Collections
- Setting data storage with Type Hinting
- Combining multiple scopes with ChainMap
- Default Dictionary values using defaultdict
- Python enums
- Creating a priority Queue
- Searching through sorted Collection
- Global Instances using Borg or Singleton patterns
- Getters and Setters with Class Properties
- Dict union operators
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
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}