Notes about Design Patterns in Python
Last Updated: November 10, 2020 by Pepe Sandoval
If you find the information in this page useful and want to show your support, you can make a donation
Use PayPal
This will help me create more stuff and fix the existent content...
Design patterns are common architectural approaches that people have observed as they've been designing object oriented software and people decided to make a catalog out of the most common ones
The opposites of pattern is an anti-pattern
self
) to chain several callsdeep.copy()
Patterns concerned with the structure of the classes. Concerned with
These type of patterns are usually wrapper patterns so they are all about the idea of making the interface has convenient to use as possible (Make objects usable and makes API is usable for other people)
If you have a class that class should have primary responsibility and should not take on other responsibilities.
Do NOT overload your classes with too many responsibilities
For example adding the responsibility of storing its data in a persistent way (the responsibility of persistence) to a class that is supposed to be an abstraction for a user, a student, a journal, etc. It would be better to have a PersitanceManager
which will be the one responsible of saving an object to a file
It enforces this idea that a class should have a single reason to change and that change should be somehow related to its primary responsibility.
The god object is an anti-pattern where you add all the functionality in a class ending up with a massive class
Different classes should handle different independent tasks/problems
OCP = open for extension but closed for modification
In a strict sense the OCP suggests that when you add new functionality you add ONLY via extension not via modification
At the implementation level the OCP suggest that after you've written and tested a particular class or code you should not modify it instead you should extend it.
If we continue to modify a class adding and adding functionality we could cause what is called state space explosion meaning we have a class with too many methods or functionality
class Specification: def is_satisfied(self, item): pass # and operator makes life easier def __and__(self, other): return AndSpecification(self, other) class Filter: def filter(self, items, spec): pass class ColorSpecification(Specification): def __init__(self, color): self.color = color def is_satisfied(self, item): return item.color == self.color class SizeSpecification(Specification): def __init__(self, size): self.size = size def is_satisfied(self, item): return item.size == self.size class AndSpecification(Specification): def __init__(self, *args): self.args = args def is_satisfied(self, item): return all(map( lambda spec: spec.is_satisfied(item), self.args)) class BetterFilter(Filter): def filter(self, items, spec): for item in items: if spec.is_satisfied(item): yield item apple = Product('Apple', Color.GREEN, Size.SMALL) tree = Product('Tree', Color.GREEN, Size.LARGE) house = Product('House', Color.BLUE, Size.LARGE) products = [apple, tree, house] bf = BetterFilter() print('Green products (new):') green = ColorSpecification(Color.GREEN) for p in bf.filter(products, green): print(f' - {p.name} is green') print('Large products:') large = SizeSpecification(Size.LARGE) for p in bf.filter(products, large): print(f' - {p.name} is large') print('Large blue items:') """ large_blue = AndSpecification(large, ColorSpecification(Color.BLUE)) """ large_blue = large & ColorSpecification(Color.BLUE) for p in bf.filter(products, large_blue): print(f' - {p.name} is large and blue')
LSP states that if you have some interface that takes some sort of base class you should be able to stick a derived class in there and everything should work.
When you have a function or interface that takes as input a base class you should be able to stick in any of its inheritor. e.g. if you have a move()
function that takes as input any object of the class Animal
if you put there any object that inherits from Animal
it should work, like Lion
, Tiger
, Dog
, etc.
you should be able to substitute a base type for a derivative type
The ISP basically says" Don't put too much into an interface, split it into separate interfaces
We don't want to put to much into a single interface because YAGNI (You aren't gonna need it)
The idea is that you don't really want to stick to many elements (too many methods) into an interface.
Making interfaces which feature too many elements is not a good idea because you're forcing your clients to define methods which they might not even need.
Instead of having one large interface with several members/methods you have separate interfaces that people can implement. Split them into the smallest interfaces you can build so that people don't have to implement more than they need to.
class Printer: @abstractmethod def print(self, document): pass class Scanner: @abstractmethod def scan(self, document): pass class MyPrinter(Printer): def print(self, document): print(document) class Photocopier(Printer, Scanner): def print(self, document): print(document) def scan(self, document): pass # something meaningful class MultiFunctionDevice(Printer, Scanner): # , Fax, etc @abstractmethod def print(self, document): pass @abstractmethod def scan(self, document): pass class MultiFunctionMachine(MultiFunctionDevice): def __init__(self, printer, scanner): self.printer = printer self.scanner = scanner def print(self, document): self.printer.print(document) def scan(self, document): self.scanner.scan(document)
DIP states that high-level modules should not depend upon low-level ones, use abstractions instead
In other words High level classes or high level modules in your code should not use directly low level modules instead they should depend on abstractions (Abstract class or class with abstract methods)
Essentially you just want to depend on interfaces so you can swap them at the low level without needing to change high-level module's code
A high-level module is a modules that uses others modules (low-level ones) which can be much closer to the hardware
Builder definition: When piecewise object construction is complicated provide an API for doing it succinctly (precisely and clearly)
The Builder pattern is used when we need to create object that requires a lot steps to initialize or a lot of arguments because it can be configured in many ways
Attempts to breakdown the whole initialization of an object into methods of a special component called the Builder
The Builder provides an API (set of functions or methods) for constructing an object in a clear and explicit way
This patterns tries to make the implementation of the construction of an object a piecewise construction instead of a having a massive call to a initializer that does everything
Required when you have several processes needed to construct something
Usually a builder will at some point call the constructor of another class to build/create an object and also store a reference to this object so the operations are perform through the builder even if they are defined in another class
class HtmlElement: indent_size = 2 def __init__(self, name="", text=""): self.name = name self.text = text self.elements = [] def __str__(self, indent): lines = [] i = ' ' * (indent * self.indent_size) lines.append(f'{i}<{self.name}>') if self.text: i1 = ' ' * ((indent + 1) * self.indent_size) lines.append(f'{i1}{self.text}') for e in self.elements: lines.append(e.__str(indent + 1)) lines.append(f'{i}{self.name}>') return '\n'.join(lines) def __str__(self): return self.__str(0) @staticmethod def create(name): return HtmlBuilder(name) class HtmlBuilder: __root = HtmlElement() def __init__(self, root_name): self.root_name = root_name self.__root.name = root_name def add_child(self, child_name, child_text): # not fluent self.__root.elements.append( HtmlElement(child_name, child_text) ) def add_child_fluent(self, child_name, child_text): # fluent self.__root.elements.append( HtmlElement(child_name, child_text) ) return self def clear(self): self.__root = HtmlElement(name=self.root_name) def __str__(self): return str(self.__root) """ ordinary non-fluent builder """ """ builder = HtmlBuilder('ul') """ builder = HtmlElement.create('ul') builder.add_child('li', 'hello') builder.add_child('li', 'world') print('Ordinary builder:') print(builder) """ fluent builder """ builder.clear() builder.add_child_fluent('li', 'hello').add_child_fluent('li', 'world') print('Fluent builder:') print(builder)
If you have an object that is very complicated to build you may need more than one builder or in other words multiple builders
For a multiple builders implementation we usually have a base builder that creates and stores an empty object that can be subsequently build up or in other words initialized
class Person: def __init__(self): print('Creating an instance of Person') # address self.street_address = None self.postcode = None self.city = None # employment info self.company_name = None self.position = None self.annual_income = None def __str__(self) -> str: ret = f'Address: {self.street_address}, ' ret += f'{self.postcode}, ' ret += f'{self.city}\n' ret += f'Employed at {self.company_name} ' ret += f'as a {self.position} ' ret += f'earning {self.annual_income}' return ret class PersonBuilder: # facade def __init__(self, person=None): if person is None: self.person = Person() else: self.person = person @property def lives(self): return PersonAddressBuilder(self.person) @property def works(self): return PersonJobBuilder(self.person) def build(self): return self.person class PersonJobBuilder(PersonBuilder): def __init__(self, person): super().__init__(person) def at(self, company_name): self.person.company_name = company_name return self def as_a(self, position): self.person.position = position return self def earning(self, annual_income): self.person.annual_income = annual_income return self class PersonAddressBuilder(PersonBuilder): def __init__(self, person): super().__init__(person) def at(self, street_address): self.person.street_address = street_address return self def with_postcode(self, postcode): self.person.postcode = postcode return self def in_city(self, city): self.person.city = city return self if __name__ == '__main__': pb = PersonBuilder() p = pb\ .lives\ .at('123 London Road')\ .in_city('London')\ .with_postcode('SW12BC')\ .works\ .at('Fabrikam')\ .as_a('Engineer')\ .earning(123000)\ .build() print(p) p2 = PersonBuilder()\ .lives\ .at('1234 Coco')\ .in_city('Guadalajara')\ .with_postcode('44865')\ .works.at('FSL MX')\ .as_a('Engineer')\ .earning(10000)\ .build() print(p2)
class Person: def __init__(self): self.name = None self.position = None self.date_of_birth = None def __str__(self): return f'{self.name} born on {self.date_of_birth} works as a {self.position}' class PersonBuilder: def __init__(self): self.person = Person() def build(self): return self.person class PersonInfoBuilder(PersonBuilder): def called(self, name): self.person.name = name return self class PersonJobBuilder(PersonInfoBuilder): def works_as_a(self, position): self.person.position = position return self class PersonBirthDateBuilder(PersonJobBuilder): def born(self, date_of_birth): self.person.date_of_birth = date_of_birth return self if __name__ == '__main__': pb = PersonBirthDateBuilder() me = pb\ .called('Pepe')\ .works_as_a('coder')\ .born('1/1/1991')\ .build() # this does NOT work in C#/C++/Java/... print(me)
Have the intention to make initializer/constructor descriptive, explicit and avoid optional parameter hell
A Factory is a component responsible solely for the complete creation of objects (not necessarily its initialization)
Types of factory implementation:
class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f'x: {self.x}, y: {self.y}' @staticmethod def new_cartesian_point(x, y): return Point(x, y) @staticmethod def new_polar_point(rho, theta): return Point(rho * sin(theta), rho * cos(theta))
# take out factory methods to a separate class class PointFactory: @staticmethod def new_cartesian_point(x, y): return Point(x, y) @staticmethod def new_polar_point(rho, theta): return Point(rho * sin(theta), rho * cos(theta))
from abc import ABC from enum import Enum, auto class HotDrink(ABC): def consume(self): pass class Tea(HotDrink): def consume(self): print('This tea is nice but I\'d prefer it with milk') class Coffee(HotDrink): def consume(self): print('This coffee is delicious') class HotDrinkFactory(ABC): def prepare(self, amount): pass class TeaFactory(HotDrinkFactory): def prepare(self, amount): print(f'Put in tea bag, boil water, pour {amount}ml, enjoy!') return Tea() class CoffeeFactory(HotDrinkFactory): def prepare(self, amount): print(f'Grind some beans, boil water, pour {amount}ml, enjoy!') return Coffee() class HotDrinkMachine: factories = {} initialized = False # being Strict this violates OCP but # we only need to change this enum for extension class AvailableDrink(Enum): COFFEE = auto() TEA = auto() def __init__(self): if not self.initialized: self.initialized = True for d in self.AvailableDrink: name = d.name[0] + d.name[1:].lower() factory_name = name + 'Factory' factory_instance = eval(factory_name)() self.factories[name.lower()] = factory_instance def make_drink(self): print('Available drinks:') for f in self.factories: print(f) name = str(input(f'Please pick drink name: ')).lower() amount = int(input(f'Specify amount: ')) return self.factories[name].prepare(amount) if __name__ == '__main__': hdm = HotDrinkMachine() drink = hdm.make_drink() drink.consume()
Comes from the idea that we don't start things from scratch we instead copy and modify or in other words we re-iterate existing designs, we look at what other people have already done and try to improve upon the existing constructs
It is a pattern that is used when there is an already existent design (can be partially or fully constructed) that you want to modify, extend, customize or complete
A Prototype is a partially or fully initialized object that you copy (clone) and make use of
Prototype pattern requires to do deep copy of objects copy.deepcopy()
You create a partially constructed object, store it somewhere, then deep copy that object, customize the copy and return this resulting instance
import copy class Address: def __init__(self, street_address, suite, city): self.suite = suite self.city = city self.street_address = street_address def __str__(self): return f'{self.street_address}, Suite #{self.suite}, {self.city}' class Employee: def __init__(self, name, address): self.address = address self.name = name def __str__(self): return f'{self.name} works at {self.address}' class EmployeeFactory: main_office_employee = Employee("", Address("123 East Dr", 0, "London")) aux_office_employee = Employee("", Address("123B East Dr", 0, "London")) @staticmethod def __new_employee(proto, name, suite): result = copy.deepcopy(proto) result.name = name result.address.suite = suite return result @staticmethod def new_main_office_employee(name, suite): return EmployeeFactory.__new_employee( EmployeeFactory.main_office_employee, name, suite ) @staticmethod def new_aux_office_employee(name, suite): return EmployeeFactory.__new_employee( EmployeeFactory.aux_office_employee, name, suite ) pepe = EmployeeFactory.new_main_office_employee("Pepe", 700) jane = EmployeeFactory.new_aux_office_employee("Jane", 200) print(pepe) print(jane)
Used for components that only makes sense to have ONLY one in the system and it makes sense to initialize them ONLY once. e.g. DB repository, the initializer call is expensive
This pattern requires that the implementation must:
A Singleton is a component/class which is instantiated only once and everybody who tries to use/access this object basically gets to work with that one instance.
import random def singleton(class_): instances = {} def get_instance(*args, **kwargs): if class_ not in instances: instances[class_] = class_(*args, **kwargs) return instances[class_] return get_instance @singleton class Database: def __init__(self): print('Loading database. Generated an id of', random.randint(1,101)) if __name__ == '__main__': d1 = Database() d2 = Database() print(d1 == d2)
import random class Singleton(type): """ Metaclass that creates a Singleton base type when called. """ _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls)\ .__call__(*args, **kwargs) return cls._instances[cls] class Database(metaclass=Singleton): def __init__(self, my_id): print('Loading database {}. Generated rand of {}'.format(my_id, random.randint(1,101))) if __name__ == '__main__': d1 = Database(1) d2 = Database(2) print(d1 == d2)
import unittest class Singleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] class Database(metaclass=Singleton): def __init__(self): self.population = {} f = open('capitals.txt', 'r') lines = f.readlines() for i in range(0, len(lines), 2): self.population[lines[i].strip()] = int(lines[i + 1].strip()) f.close() class SingletonRecordFinder: def total_population(self, cities): result = 0 for c in cities: result += Database().population[c] return result class ConfigurableRecordFinder: def __init__(self, db): self.db = db def total_population(self, cities): result = 0 for c in cities: result += self.db.population[c] return result class DummyDatabase: population = { 'alpha': 1, 'beta': 2, 'gamma': 3 } def get_population(self, name): return self.population[name] class SingletonTests(unittest.TestCase): """ These test on a live database :( """ """ # require capitals.txt to work def test_is_singleton(self): db = Database() db2 = Database() self.assertEqual(db, db2) def test_singleton_total_population(self): rf = SingletonRecordFinder() names = ['Seoul', 'Mexico City'] tp = rf.total_population(names) self.assertEqual(tp, 17500000 + 17400000) # what if these change? """ def test_dependent_total_population(self): ddb = DummyDatabase() crf = ConfigurableRecordFinder(ddb) self.assertEqual(crf.total_population(['alpha', 'beta']), 3) if __name__ == '__main__': unittest.main()
The Adapter pattern tries to adapt the interface that you are given to the interface that you actually need/want.
The adapter works as a component that give us the interface we require from the interface we have
Adapter is a SW construct that adapts existent interface X to conform to the required interface Y
The adapter is an in-between, but separate, component/class to interface two components (classes)
To implement an adapter first identify the API/class you have and the API/class/method/function you need, then create a component, the adapter, that has a reference to the API/class you have, the adaptee, it wraps the API/class you have and uses it, finally in your system or application you use the adapter which internally will end up using the adaptee
class Point: def __init__(self, x, y): self.y = y self.x = x def __str__(self): return f"(x={self.x},y={self.y})" def draw_point(p): print('*', end='') """ you are given this API and you cannot change it """ class Line: def __init__(self, start, end): self.end = end self.start = start class Rectangle(list): """ Represented as a list of lines. """ def __init__(self, x, y, width, height): super().__init__() self.append(Line(Point(x, y), Point(x + width, y))) self.append(Line(Point(x + width, y), Point(x + width, y + height))) self.append(Line(Point(x, y), Point(x, y + height))) self.append(Line(Point(x, y + height), Point(x + width, y + height))) class LineToPointAdapter(list): count = 0 def __init__(self, line): left = min(line.start.x, line.end.x) right = max(line.start.x, line.end.x) top = max(line.start.y, line.end.y) bottom = min(line.start.y, line.end.y) LineToPointAdapter.count += 1 out = f'{self.count}: Generating points for line ' out += f'[{line.start.x},{line.start.y}]→' out += f'[{line.end.x},{line.end.y}] ' out += f'left: {left} ; right: {right} ' out += f'top: {top} ; bottom: {bottom} ' out += '| points for line: ' if right == left: for y in range(bottom, top+1): p = Point(left, y) out += str(p) + " " self.append(p) elif top == bottom : for x in range(left, right+1): p = Point(x, top) out += str(p) + " " self.append(p) print(out) def draw(rcs): print("\n\n--- Drawing some stuff ---\n") for rc in rcs: for line in rc: # No cache = We create repeated objects adapter = LineToPointAdapter(line) for p in adapter: draw_point(p) print("") if __name__ == '__main__': rs = [Rectangle(1, 1, 10, 10), Rectangle(3, 3, 6, 6)] draw(rs) draw(rs)
class Point: def __init__(self, x, y): self.y = y self.x = x def __str__(self): return f"(x={self.x},y={self.y})" def draw_point(p): print('.', end='') class Line: def __init__(self, start, end): self.end = end self.start = start class Rectangle(list): """ Represented as a list of lines. """ def __init__(self, x, y, width, height): super().__init__() self.append(Line(Point(x, y), Point(x + width, y))) self.append(Line(Point(x + width, y), Point(x + width, y + height))) self.append(Line(Point(x, y), Point(x, y + height))) self.append(Line(Point(x, y + height), Point(x + width, y + height))) class LineToPointAdapter: count = 0 cache = {} def __init__(self, line): self.h = hash(line) if self.h in LineToPointAdapter.cache: return super().__init__() points = [] left = min(line.start.x, line.end.x) right = max(line.start.x, line.end.x) top = max(line.start.y, line.end.y) bottom = min(line.start.y, line.end.y) LineToPointAdapter.count += 1 out = f'{LineToPointAdapter.count}: Generating points for line ' out += f'[{line.start.x},{line.start.y}]→' out += f'[{line.end.x},{line.end.y}] ' out += f'left: {left} ; right: {right} ' out += f'top: {top} ; bottom: {bottom} ' out += '| points for line: ' if right == left: for y in range(bottom, top+1): p = Point(left, y) out += str(p) + " " points.append(p) elif top == bottom : for x in range(left, right+1): p = Point(x, top) out += str(p) + " " points.append(p) print(out) LineToPointAdapter.cache[self.h] = points def __iter__(self): return iter(LineToPointAdapter.cache[self.h]) def draw(rcs): print('Drawing some rectangles...') for rc in rcs: for line in rc: adapter = LineToPointAdapter(line) for p in adapter: draw_point(p) print('') print('\n') if __name__ == '__main__': rs = [ Rectangle(1, 1, 10, 10), Rectangle(3, 3, 6, 6) ] draw(rs) draw(rs) # can define your own hashes or use the defaults #print(hash(Line(Point(1, 1), Point(10, 10))))
Used to connect components through abstractions
This pattern is used to prevent Entity Explosion or Cartesian Product Complexity Explosion which happens when you only rely on inheritance and if you have 2 variables that can be combined you end up with 4 classes e.g. a preemptive and non-preemptive scheduler that can be implemented differently in Linux and Windows so you end up with 4 classes
The Bridge patterns used inheritance plus aggregation to decouple the interface from the actual implementation
The bridge helps connect to class hierarchies by storing a reference to the base of one of the hierarchies in the other one and use it in the derivative classes, when we create these derivatives we will need to pass an derivative object that will be stored through the reference we mentioned before
For scaling we don't need to implement new classes for each combination however we do need to implement a new class and extend the methods/functionality on the hierarchy of classes that is stored as a reference
# Circles and squares # Each can be rendered in vector or raster form class Renderer(): def render_circle(self, radius): pass def render_square(self, side): pass # For new class we need to extend this class VectorRenderer(Renderer): def render_circle(self, radius): print(f'Drawing a circle of radius {radius}') def render_square(self, side): print(f'Drawing a square of side {side}') # Also extend here class RasterRenderer(Renderer): def render_circle(self, radius): print(f'Drawing pixels for circle of radius {radius}') def render_square(self, side): print(f'Drawing pixels for square of side {side}') # And extend here class Shape: def __init__(self, renderer): self.renderer = renderer def draw(self): pass def resize(self, factor): pass class Circle(Shape): def __init__(self, renderer, radius): super().__init__(renderer) self.radius = radius def draw(self): self.renderer.render_circle(self.radius) def resize(self, factor): self.radius *= factor class Square(Shape): def __init__(self, renderer, side): super().__init__(renderer) self.side = side def draw(self): self.renderer.render_square(self.side) def resize(self, factor): self.side *= factor # We will need to create a new class if __name__ == '__main__': raster = RasterRenderer() vector = VectorRenderer() circle = Circle(vector, 5) circle.draw() circle.resize(2)
The goal of the composite design patterns is to treat individual component and groups of objects in a uniform way so to provide and identical interface for both aggregates and individual components
The composite looks to treat individual components and aggregate objects in the same way, for example if you have a drawing app that allows the user to move a Shape
most likely the user will want to move a group of Shape
objects so at the implementation level we will want to use the same interface/method/mechanism to do this
The composite design pattern is used to treat both single (scalar) and composite objects (groups of objects) in exactly the same way
It is a mechanism for treating individual objects and groups of objects in a uniform manner
Used for cases where composed and singular objects actually need to provide similar or identical behavior. In cases where you want to be able to treat a group of objects using exactly the same interface as a single object we use the composite pattern
class GraphicObject: def __init__(self, color=None): self.color = color self.children = [] self._name = 'Group' @property def name(self): return self._name def _print(self, items, depth): items.append('*' * depth) if self.color: items.append(self.color) items.append(f'{self.name}\n') for child in self.children: child._print(items, depth + 1) def __str__(self): items = [] self._print(items, 0) return ''.join(items) class Circle(GraphicObject): @property def name(self): return 'Circle' class Square(GraphicObject): @property def name(self): return 'Square' if __name__ == '__main__': drawing = GraphicObject() drawing._name = 'My Drawing' drawing.children.append(Square('Red')) drawing.children.append(Circle('Yellow')) group = GraphicObject() # no name group.children.append(Circle('Blue')) group.children.append(Square('Blue')) drawing.children.append(group) print(drawing)
from abc import ABC from collections.abc import Iterable class Connectable(Iterable, ABC): def connect_to(self, other): if self == other: return for s in self: for o in other: s.outputs.append(o) o.inputs.append(s) class Neuron(Connectable): def __init__(self, name): self.name = name self.inputs = [] self.outputs = [] def __iter__(self): yield self # This turns a scalar object into something that's iterable def __str__(self): return 'name: {}. {} inputs: ({}) ; {} outputs: ({}) '.format(self.name, len(self.inputs), [n.name for n in self.inputs], len(self.outputs), [n.name for n in self.outputs]) class NeuronLayer(list, Connectable): def __init__(self, name, count): super().__init__() self.name = name for x in range(0, count): self.append(Neuron(f'{name}-{x}')) def __str__(self): return '{} with {} neurons:\n {}'.format(self.name, len(self), "\n ".join([str(n) for n in self])) if __name__ == '__main__': neuron1 = Neuron('n1') neuron2 = Neuron('n2') layer1 = NeuronLayer('L1', 3) layer2 = NeuronLayer('L2', 4) neuron1.connect_to(neuron2) neuron1.connect_to(layer1) layer1.connect_to(neuron2) layer1.connect_to(layer2) print(neuron1) print(neuron2) print(layer1) print(layer2)
The Decorator design pattern exists so that you can add additional behaviors to particular classes or functions without either modifying the classes themselves or indeed inheriting from them.
It is used when you want to augment a class with extra features but don't want to rewrite or override existing code (as you would do through inheritance)
To interact with existent structures a decorator references the decorated object(s) or function(s), so you have a reference and offer additional operations on top of that
A decorators facilitates the addition of behavior to individual objects without inheriting from them
A decorator keeps a reference to the decorated object(s), the objects it actually decorates, then it adds a certain utility methods and attributes to increase the objects functionality
The implementation of decorator inside python is what is known as a functional decorator which is very specific implementation of the general purpose decorator
The functional decorator are used for performing something around a function. So you take a function or indeed a method and what you do is you use the decorators to actually perform some initialization code, perform some termination code and even store some values if you want.
Python provides special syntax to apply a functional decorator using the @
symbol
import time def time_it(func): def wrapper(): start = time.time() result = func() end = time.time() print(f'{func.__name__} took {int((end-start)*1000)}ms') return wrapper def some_op_without_at(): print('Starting op') time.sleep(1) print('We are done') return 123 @time_it def some_op_with_at(): print('Starting op') time.sleep(1) print('We are done') return 123 if __name__ == '__main__': # Without using @ time_it(some_op_without_at)() print("") # Using @ special python notation some_op_with_at()
You built a class that augments the functionality of the existing class and keep a reference to the class you are augmenting (NOT inherit from the class you augment)
Classical decorators is a class which takes the decorated object as an argument, usually also takes some additional values and it provides the extra functionality
from abc import ABC class Shape(ABC): def __str__(self): return '' class Circle(Shape): def __init__(self, radius=0.0): self.radius = radius def resize(self, factor): self.radius *= factor def __str__(self): return f'A circle of radius {self.radius}' class Square(Shape): def __init__(self, side): self.side = side def __str__(self): return f'A square with side {self.side}' class ColoredShape(Shape): def __init__(self, shape, color): if isinstance(shape, ColoredShape): raise Exception('Cannot apply ColoredDecorator twice') self.shape = shape self.color = color def __str__(self): return f'{self.shape} has the color {self.color}' class TransparentShape(Shape): def __init__(self, shape, transparency): self.shape = shape self.transparency = transparency def __str__(self): return f'{self.shape} has {self.transparency * 100.0}% transparency' if __name__ == '__main__': circle = Circle(2) print(circle) red_circle = ColoredShape(circle, "red") print(red_circle) # ColoredShape doesn't have resize() # red_circle.resize(3) # We cannot do this red_half_transparent_square = TransparentShape(red_circle, 0.5) print(red_half_transparent_square) # prevent double application at ColoredShape constructor mixed = ColoredShape(ColoredShape(Circle(3), 'red'), 'blue') print(mixed)
The idea of a dynamic decorator is to wrap an object (reference to an object), add new attributes and methods but also forward calls to the underlying object or stored reference to use the attributes and methods of this object when we want to access methods and attributes we don't define in the decorator
At the implementation level it means to have a reference to an object and override the methods __iter__
, __next__
, __getattr__
, __setattr__
, __delattr__
, that way we can use all the methods and attributes from the reference when using the decorator
class FileWithLogging: def __init__(self, file): self.file = file def writelines(self, strings): self.file.writelines(strings) print(f'wrote {len(strings)} lines') def __iter__(self): return self.file.__iter__() def __next__(self): return self.file.__next__() def __getattr__(self, item): return getattr(self.__dict__['file'], item) def __setattr__(self, key, value): if key == 'file': self.__dict__[key] = value else: setattr(self.__dict__['file'], key) def __delattr__(self, item): delattr(self.__dict__['file'], item) if __name__ == '__main__': file = FileWithLogging(open('hello.txt', 'w')) file.writelines(['hello', 'world']) file.write('testing') file.close()
The main idea is to expose several components through a single interface to balance complexity and presentation/usability
The Façade tries to provide a simple and easy to use API over a large and sophisticated body of code that could involve several classes and functions
The face tries to expose a single function or object that you call to have a lots of things happen underneath
A Facade is build to provide a simplified API over a set of classes/components, optionally expose internal through the facade
class Buffer: def __init__(self, width=30, height=20): self.width = width self.height = height self.buffer = [' '] * (width*height) def __getitem__(self, item): return self.buffer.__getitem__(item) def write(self, text): self.buffer += text class Viewport: def __init__(self, buffer=Buffer()): self.buffer = buffer self.offset = 0 def get_char_at(self, index): return self.buffer[self.offset+index] def append(self, text): self.buffer += text # this is the facade class Console: def __init__(self): b = Buffer() self.current_viewport = Viewport(b) self.buffers = [b] self.viewports = [self.current_viewport] # high-level def write(self, text): self.current_viewport.buffer.write(text) # low-level def get_char_at(self, index): return self.current_viewport.get_char_at(index) if __name__ == '__main__': c = Console() c.write('hello') ch = c.get_char_at(600) print(ch)
The Flyweight is a space optimization technique which means it is a pattern that tries to save memory
The goal of the Flyweight design patterns is to avoid redundancy when storing data, in other words avoid duplication of data. The patterns instead will look to store only references to a data structure shared among many components instead of replicating and storing the data in different components. e.g. just store database ID where the data is stored
It is a space optimization technique that lets us use less memory by storing externally the data associated with similar components/objects
import random import string import sys class User: # Will duplicate storage of strings def __init__(self, name): self.name = name class UserFlyweight: # Won't duplicate storage of strings strings = [] def __init__(self, full_name): def get_or_add(s): if s in self.strings: return self.strings.index(s) else: self.strings.append(s) return len(self.strings)-1 self.names = [get_or_add(x) for x in full_name.split(' ')] print("Created user {} with indexes {} user: {}".format(id(self), ":".join([str(i) for i in self.names]), str(self))) def __str__(self): return ' '.join([self.strings[x] for x in self.names]) def random_string(): chars = string.ascii_lowercase return ''.join([random.choice(chars) for x in range(8)]) if __name__ == '__main__': USERS_NUM = 2 users = [] u2 = UserFlyweight('Jim Jones') u3 = UserFlyweight('Frank Jones') first_names = [random_string() for x in range(USERS_NUM//2)] last_names = [random_string() for x in range(USERS_NUM//2)] for first in first_names: for last in last_names: usr = UserFlyweight(f'{first} {last}') users.append(usr) print("Strings:", UserFlyweight.strings)
class FormattedText: def __init__(self, plain_text): self.plain_text = plain_text self.caps = [False] * len(plain_text) def capitalize(self, start, end): for i in range(start, end): self.caps[i] = True def __str__(self): result = [] for i in range(len(self.plain_text)): c = self.plain_text[i] result.append(c.upper() if self.caps[i] else c) return ''.join(result) class BetterFormattedText: class TextRange: # This is the Flyweight def __init__(self, start, end, capitalize=False, bold=False, italic=False): self.end = end self.bold = bold self.capitalize = capitalize self.italic = italic self.start = start def covers(self, position): return self.start <= position <= self.end def __init__(self, plain_text): self.plain_text = plain_text self.formatting = [] def get_range(self, start, end): range = self.TextRange(start, end) self.formatting.append(range) return range # return the Flyweight def __str__(self): result = [] for i in range(len(self.plain_text)): c = self.plain_text[i] for r in self.formatting: if r.covers(i) and r.capitalize: c = c.upper() result.append(c) return ''.join(result) if __name__ == '__main__': ft = FormattedText('This is a brave new world') ft.capitalize(10, 15) print(ft) bft = BetterFormattedText('This is a brave new world') bft.get_range(16, 19).capitalize = True bft.get_range(0, 3).capitalize = True print(bft)
A Proxy is a class that functions as an interface to a particular resource. That resource could be other component that can be remote, expensive to construct, require some other functionality, etc
The intention of the proxy is to provide the same interface as the component it wraps but with an entirely different behavior or with added functionality, so to create proxy first replicate the existing interface of an object and then add relevant functionality
class Car: def __init__(self, driver): self.driver = driver def drive(self): print(f'Car being driven by {self.driver.name}') class CarProxy: def __init__(self, driver): self.driver = driver self.car = Car(driver) def drive(self): if self.driver.age >= 18: self.car.drive() else: print('Driver too young') class Driver: def __init__(self, name, age): self.name = name self.age = age if __name__ == '__main__': car = CarProxy(Driver('John', 12)) car.drive()
It is a proxy that appears to be the underlying object. So it appears to be a fully initialized object but in actual fact it's not, it's just masquerading the underlying functionality
By virtual it means that for all intents and purposes it appears like the object it's supposed to represent but behind the scenes it can offer additional functionality and behave differently
class Bitmap: def __init__(self, filename): self.filename = filename print(f'Loading image from {filename}') def draw(self): print(f'Drawing image {self.filename}') class LazyBitmap: # This is the Virtual Proxy def __init__(self, filename): self.filename = filename self.bitmap = None def draw(self): if not self.bitmap: self.bitmap = Bitmap(self.filename) self.bitmap.draw() def draw_image(image): print('About to draw image') image.draw() print('Done drawing image') if __name__ == '__main__': bmp = LazyBitmap('facepalm.jpg') # Bitmap draw_image(bmp)
The proxy provides an identical interface to the object that its proxy things to (the objects is uses) whereas the decorator provides an enhanced interface.
A decorator may replicate some of its API s or indeed all of its API but it is designed to add additional operations or functionality
A decorator usually has a reference to the object that is decorating, taking that objects as a constructor argument the proxy usually doesn't take this reference as argument but constructs the object if is needed
Chain of Responsibility refers to a chain of components who all get a chance to process a command or query, optionally with as default processing implementation and ability yo terminate the processing chain
It is a design pattern used to describe what component should be responsible of certain processing when we have multiple components that have a relation to each other, usually in a hierarchy, for example a button inside a group of buttons inside a window
This can be implemented as a linked list of references typically references to methods/functions
Command-Query Separation (CQS) refers to the idea that whenever we operate on objects we separate invocations into query and command so we have separate means to send commands and queries
By using the CQS and the Chain of responsibility you can have listeners to the commands and queries to do something or even override the behaviors of the command or query
class Creature: def __init__(self, name, attack, defense): self.defense = defense self.attack = attack self.name = name def __str__(self): return f'{self.name} ({self.attack}/{self.defense})' class CreatureModifier: def __init__(self, creature): self.creature = creature self.next_modifier = None def add_modifier(self, modifier): if self.next_modifier: self.next_modifier.add_modifier(modifier) else: self.next_modifier = modifier def remove_modifier(self, modifier): if self.next_modifier and modifier is self.next_modifier: self.next_modifier = modifier.next_modifier del(modifier) elif self.next_modifier: self.next_modifier.remove_modifier(modifier) def handle(self): if self.next_modifier: self.next_modifier.handle() class NoBonusesModifier(CreatureModifier): def handle(self): print('No bonuses for you!') class DoubleAttackModifier(CreatureModifier): def handle(self): print(f"Doubling {self.creature.name}'s attack with modifier {id(self)}") self.creature.attack *= 2 super().handle() class DoubleDefenseModifier(CreatureModifier): def handle(self): print(f"Doubling {self.creature.name}'s defense with modifier {id(self)}") self.creature.defense *= 2 super().handle() if __name__ == '__main__': goblin = Creature('Goblin', 1, 1) print(goblin) root = CreatureModifier(goblin) # If you uncomment this will stop the chain of # responsability by not calling base class handle() #root.add_modifier(NoBonusesModifier(goblin)) root.add_modifier(DoubleAttackModifier(goblin)) root.add_modifier(DoubleAttackModifier(goblin)) to_rem = DoubleAttackModifier(goblin) root.add_modifier(to_rem) root.remove_modifier(to_rem) root.add_modifier(DoubleDefenseModifier(goblin)) root.handle() # apply modifiers print(goblin)
# 1) event broker # 2) command-query separation (cqs) # 3) observer from abc import ABC from enum import Enum class Event(list): def __call__(self, *args, **kwargs): for item in self: item(*args, **kwargs) class WhatToQuery(Enum): ATTACK = 1 DEFENSE = 2 class Query: def __init__(self, creature_name, what_to_query, default_value): self.value = default_value # bidirectional self.what_to_query = what_to_query self.creature_name = creature_name class Game: # This is the event broker def __init__(self): self.queries = Event() def perform_query(self, sender, query): self.queries(sender, query) class Creature: def __init__(self, game, name, attack, defense): self.initial_defense = defense self.initial_attack = attack self.name = name self.game = game @property def attack(self): q = Query(self.name, WhatToQuery.ATTACK, self.initial_attack) self.game.perform_query(self, q) return q.value @property def defense(self): q = Query(self.name, WhatToQuery.DEFENSE, self.initial_attack) self.game.perform_query(self, q) return q.value def __str__(self): return f'{self.name} ({self.attack}/{self.defense})' class CreatureModifier(ABC): def __init__(self, game, creature): self.creature = creature self.game = game self.game.queries.append(self.handle) def handle(self, sender, query): pass def __enter__(self): # Executed when you enter a `with` block, what you return here is what is assigned to identifier after `as` return self def __exit__(self, exc_type, exc_val, exc_tb): # Executed when you exit a `with` block self.game.queries.remove(self.handle) class DoubleAttackModifier(CreatureModifier): def handle(self, sender, query): if (sender.name == self.creature.name and query.what_to_query == WhatToQuery.ATTACK): query.value *= 2 class IncreaseDefenseModifier(CreatureModifier): def handle(self, sender, query): if (sender.name == self.creature.name and query.what_to_query == WhatToQuery.DEFENSE): query.value += 3 if __name__ == '__main__': game = Game() goblin = Creature(game, 'Dark Goblin', 2, 2) print(goblin) dam = DoubleAttackModifier(game, goblin) idm = IncreaseDefenseModifier(game, goblin) print(goblin) goblin2 = Creature(game, 'Light Goblin', 2, 2) print(goblin2) with DoubleAttackModifier(game, goblin2): print(goblin2) with IncreaseDefenseModifier(game, goblin2): print(goblin2) print(goblin2) print(goblin2)
Used to define an object that represents an operation and record this operation actually took place
A Command is an object that represents an instruction/operation to perform a particular action. This object usually contains all the information necessary for the action to be taken
To implement this you encapsulate all the details of an operation in an object, then you define the instruction for applying the command invoke
& undo
which will call the methods to perform the actual operation and always use the instruction to perform operations
from abc import ABC from enum import Enum class BankAccount: OVERDRAFT_LIMIT = -500 def __init__(self, balance=0): self.balance = balance def deposit(self, amount): self.balance += amount print(f'Deposited {amount}, balance = {self.balance}') def withdraw(self, amount): if self.balance - amount >= BankAccount.OVERDRAFT_LIMIT: self.balance -= amount print(f'Withdrew {amount}, balance = {self.balance}') return True return False def __str__(self): return f'Balance = {self.balance}' class Command(ABC): def invoke(self): pass def undo(self): pass class BankAccountCommand(Command): def __init__(self, account, action, amount): self.amount = amount self.action = action self.account = account self.success = None class Action(Enum): DEPOSIT = 0 WITHDRAW = 1 def invoke(self): if self.action == self.Action.DEPOSIT: self.account.deposit(self.amount) self.success = True elif self.action == self.Action.WITHDRAW: self.success = self.account.withdraw(self.amount) def undo(self): if not self.success: return # strictly speaking this is not correct # (you don't undo a deposit by withdrawing) # but it works for this demo, so... if self.action == self.Action.DEPOSIT: self.account.withdraw(self.amount) elif self.action == self.Action.WITHDRAW: self.account.deposit(self.amount) if __name__ == '__main__': ba = BankAccount() cmd = BankAccountCommand(ba, BankAccountCommand.Action.DEPOSIT, 100) cmd.invoke() print('After $100 deposit:', ba) cmd.undo() print('$100 deposit undone:', ba) illegal_cmd = BankAccountCommand(ba, BankAccountCommand.Action.WITHDRAW, 1000) illegal_cmd.invoke() print('After impossible withdrawal:', ba) illegal_cmd.undo() print('After undo:', ba)
from abc import ABC from enum import Enum class BankAccount: OVERDRAFT_LIMIT = 0 def __init__(self, balance=0, name_id=""): self.balance = balance self.name_id = name_id def deposit(self, amount): self.balance += amount print(f'Deposited {amount} from account with id {self.name_id}, balance = {self.balance}') def withdraw(self, amount): if self.balance - amount >= BankAccount.OVERDRAFT_LIMIT: self.balance -= amount print(f'Withdrew {amount} from account with id {self.name_id}, balance = {self.balance}') return True return False def __str__(self): return f'Balance = {self.balance}' class Command(ABC): def __init__(self): self.success = False def invoke(self): pass def undo(self): pass class BankAccountCommand(Command): def __init__(self, account, action, amount): super().__init__() self.amount = amount self.action = action self.account = account class Action(Enum): DEPOSIT = 0 WITHDRAW = 1 def invoke(self): if self.action == self.Action.DEPOSIT: self.account.deposit(self.amount) self.success = True elif self.action == self.Action.WITHDRAW: self.success = self.account.withdraw(self.amount) def undo(self): if not self.success: return # strictly speaking this is not correct # (you don't undo a deposit by withdrawing) # but it works for this demo, so... if self.action == self.Action.DEPOSIT: self.account.withdraw(self.amount) elif self.action == self.Action.WITHDRAW: self.account.deposit(self.amount) # try using this before using MoneyTransferCommand! class CompositeBankAccountCommand(Command, list): def __init__(self, items=[]): super().__init__() for i in items: self.append(i) def invoke(self): for x in self: x.invoke() def undo(self): for x in reversed(self): x.undo() class MoneyTransferCommand(CompositeBankAccountCommand): def __init__(self, from_acct, to_acct, amount): super().__init__([ BankAccountCommand(from_acct, BankAccountCommand.Action.WITHDRAW, amount), BankAccountCommand(to_acct, BankAccountCommand.Action.DEPOSIT, amount)]) def invoke(self): ok = True for cmd in self: if ok: cmd.invoke() ok = cmd.success else: cmd.success = False self.success = ok def undo(self): ok = True for cmd in reversed(self): if ok: cmd.undo() ok = cmd.success else: cmd.success = False self.success = ok def test_composite_deposit(): ba = BankAccount() deposit1 = BankAccountCommand(ba, BankAccountCommand.Action.DEPOSIT, 1000) deposit2 = BankAccountCommand(ba, BankAccountCommand.Action.DEPOSIT, 1000) composite = CompositeBankAccountCommand([deposit1, deposit2]) composite.invoke() print(ba) composite.undo() print(ba) def test_transfer_fail(): ba1 = BankAccount(100) ba2 = BankAccount() # composite isn't so good because of failure amount = 1000 # try 1000: no transactions should happen wc = BankAccountCommand(ba1, BankAccountCommand.Action.WITHDRAW, amount) dc = BankAccountCommand(ba2, BankAccountCommand.Action.DEPOSIT, amount) transfer = CompositeBankAccountCommand([wc, dc]) transfer.invoke() print('ba1:', ba1, 'ba2:', ba2) # end up in incorrect state transfer.undo() print('ba1:', ba1, 'ba2:', ba2) def test_better_tranfer(): ba1 = BankAccount(100, "BA1") ba2 = BankAccount(0, "BA2") amount = 10 transfer = MoneyTransferCommand(ba1, ba2, amount) transfer.invoke() print('ba1:', ba1, 'ba2:', ba2) print(transfer.success) transfer.undo() print('ba1:', ba1, 'ba2:', ba2) print(transfer.success) if __name__ == '__main__': test_better_tranfer()
Used to process/interpret textual input (e.g. process HTML, XML or simple math expression 3+4/5). It has the goal of turning strings or plain text into data structures
The Interpreter is a component that processes structured text data, it does this in two stages: lexing and parsing
from enum import Enum class Token: class Type(Enum): INTEGER = 0 PLUS = 1 MINUS = 2 LPAREN = 3 RPAREN = 4 def __init__(self, type, text): self.type = type self.text = text def __str__(self): return f'`{self.text}`' # ↓↓↓ lexing ↓↓↓ def lex(input): result = [] i = 0 while i < len(input): if input[i] == '+': result.append(Token(Token.Type.PLUS, '+')) elif input[i] == '-': result.append(Token(Token.Type.MINUS, '-')) elif input[i] == '(': result.append(Token(Token.Type.LPAREN, '(')) elif input[i] == ')': result.append(Token(Token.Type.RPAREN, ')')) else: # must be a number digits = [input[i]] for j in range(i + 1, len(input)): if input[j].isdigit(): digits.append(input[j]) i += 1 else: result.append(Token(Token.Type.INTEGER, ''.join(digits))) break i += 1 return result # ↓↓↓ parsing ↓↓↓ class Integer: def __init__(self, value): self.value = value def __str__(self): return f"Integer {self.value}" class BinaryOperation: class Type(Enum): ADDITION = 0 SUBTRACTION = 1 def __init__(self): self.type = None self.left = None self.right = None @property def value(self): left = self.left.value right = self.right.value print(f"Calling value of Binary Op with ID {id(self)} left: {left} ; right: {right}") if self.type == self.Type.ADDITION: return left + right elif self.type == self.Type.SUBTRACTION: return left - right def parse(tokens): result = BinaryOperation() have_lhs = False i = 0 while i < len(tokens): token = tokens[i] if token.type == Token.Type.INTEGER: integer = Integer(int(token.text)) if not have_lhs: result.left = integer have_lhs = True else: result.right = integer elif token.type == Token.Type.PLUS: result.type = BinaryOperation.Type.ADDITION elif token.type == Token.Type.MINUS: result.type = BinaryOperation.Type.SUBTRACTION elif token.type == Token.Type.LPAREN: # note: no if for RPAREN j = i while j < len(tokens): if tokens[j].type == Token.Type.RPAREN: break j += 1 # preprocess subexpression subexpression = tokens[i + 1:j] element = parse(subexpression) if not have_lhs: result.left = element have_lhs = True else: result.right = element i = j # advance i += 1 return result def my_eval(input): tokens = lex(input) print(' '.join(map(str, tokens))) parsed = parse(tokens) print(f'{input} = {parsed.value}') if __name__ == '__main__': my_eval('(13+4)-(12+1)') my_eval('1+(3-4)') # This won't work print("Following evaluation will be wrong") my_eval('1+2+(3-4)')
Iteration is a core functionality of various data structures
An Iterator is a class that facilitates a mechanism to traverse a data structure, it specified how to traverse an object. This is done through two other mechanism:
In Python if you want an object to be iterable it needs to have an __iter__
method, which needs to expose and iterator while the iterator need to have a __next__
method which returns each of the iterated elements, every time is called you return the current element until you have nothing to return (reach the end) in that case you raise a stop iteration (raise StopIteration
)
__iter__
and __next__
A Stateful iterator changes the current element it stores as reference so every time you call it changes this reference. These type of iterators cannot be recursive
class Node: def __init__(self, value, left=None, right=None): self.right = right self.left = left self.value = value self.parent = None if left: self.left.parent = self if right: self.right.parent = self def __iter__(self): return InOrderIterator(self) class InOrderIterator: def __init__(self, root): self.root = self.current = root self.yielded_start = False while self.current.left: self.current = self.current.left def __next__(self): if not self.yielded_start: self.yielded_start = True return self.current if self.current.right: self.current = self.current.right while self.current.left: self.current = self.current.left return self.current else: p = self.current.parent while p and self.current == p.right: self.current = p p = p.parent self.current = p if self.current: return self.current else: raise StopIteration def traverse_in_order(root): def traverse(current): if current.left: for left in traverse(current.left): yield left yield current if current.right: for right in traverse(current.right): yield right for node in traverse(root): yield node if __name__ == '__main__': # 1 # / \ # 2 3 # in-order: 213 # preorder: 123 # postorder: 231 root = Node(1, Node(2), Node(3)) it = iter(root) print([next(it).value for x in range(3)]) for x in root: print(x.value) for y in traverse_in_order(root): print(y.value)
class Creature: strength_index = 0 intelligence_index = 1 agility_index = 2 def __init__(self, name, strength=10, intelligence=10, agility=10): self.stats = [strength, intelligence, agility] self.name = name @property def strength(self): return self.stats[Creature.strength_index] @strength.setter def strength(self, value): self.stats[Creature.strength_index] = value @property def intelligence(self): return self.stats[Creature.intelligence_index] @intelligence.setter def intelligence(self, value): self.stats[Creature.intelligence_index] = value @property def agility(self): return self.stats[Creature.agility_index] @agility.setter def agility(self, value): self.stats[Creature.agility_index] = value # Operational methods, could also be properties def sum_stats(self): return sum(self.stats) def max_stats(self): return max(self.stats) def min_stats(self): return min(self.stats) def average_stats(self): return self.sum_stats()/len(self.stats) def __str__(self): c_str = f'''{self.name} with strength: {self.strength} intelligence: {self.intelligence} agility: {self.agility} stats: sum: {self.sum_stats()} max: {self.max_stats()} min: {self.min_stats()} avg: {self.average_stats()} ''' return c_str if __name__ == '__main__': c1 = Creature("Ork") print(c1)
A Mediator is a component that facilitates communication between other componentts without them necessarily being aware of each other or having direct access/reference to each other
It used to facilitate communication between components, since components are always going in and out of systems the mediator functions as central component used for communication
To implement a mediator:
class Person: def __init__(self, name): self.name = name self.chat_log = [] self.room = None def receive(self, sender, message): s = f'{sender}: {message}' print(f'[{self.name}\'s chat session] {s}') self.chat_log.append(s) def say(self, message): self.room.broadcast(source=self.name, message=message) def private_message(self, who, message): self.room.message(source=self.name, destination=who, message=message) class ChatRoom: # This is the mediator def __init__(self): self.people = [] def broadcast(self, source, message): for p in self.people: if p.name != source: p.receive(sender=source, message=message) def join(self, person): join_msg = f'{person.name} joins the chat' self.broadcast(source='room', message=join_msg) person.room = self self.people.append(person) def leave(self, person): if person in self.people: leave_msg = f'{person.name} leaves the chat' person.room = None self.people.remove(person) self.broadcast(source='room', message=leave_msg) def message(self, source, destination, message): for p in self.people: if p.name == destination: p.receive(sender=source, message=message) if __name__ == '__main__': room = ChatRoom() john = Person('John') jane = Person('Jane') room.join(john) john.say('hi room') # This won't show since nobody else is in the room at this point room.join(jane) john.say('hi room again') jane.say('oh, hey john') simon = Person('Simon') room.join(simon) simon.say('hi everyone!') jane.private_message('Simon', 'glad you could join us!') room.leave(john)
class Event(list): def __call__(self, *args, **kwargs): for item in self: item(*args, **kwargs) class Game: def __init__(self): self.events = Event() def fire(self, args): self.events(args) class GoalScoredInfo: def __init__(self, who_scored, goals_scored): self.goals_scored = goals_scored self.who_scored = who_scored class Player: def __init__(self, name, game): self.name = name self.game = game self.goals_scored = 0 def score(self): self.goals_scored += 1 args = GoalScoredInfo(self.name, self.goals_scored) self.game.fire(args) class Coach: def __init__(self, game): game.events.append(self.celebrate_goal) def celebrate_goal(self, args): if isinstance(args, GoalScoredInfo): if args.goals_scored == 3: print(f'Coach says: You are on fire {args.who_scored}!') elif args.goals_scored > 1: print(f'Coach says: well done, {args.who_scored}!') else: pass # coach not impressed after more than 3 goals or just 1 goal if __name__ == '__main__': game = Game() player = Player('Sam', game) coach = Coach(game) player.score() # ignored by coach player.score() # Coach says: well done, Sam! player.score() # Coach says: You are on fire Sam!
The Memento is a token or handle representing the system state at a point in time, it lets us roll back to this state and could expose state information. So it just a class that typically has no methods and just stores data
This pattern is used to keep a memento or snapshot of an object's state to return to that state or just preserve that information
To implement it the idea is that every time there is a change on the system you return a token of the current state so that you can restore the system back to the state contained in the token
class Memento: def __init__(self, balance): self.balance = balance class BankAccount: def __init__(self, balance=0): self.balance = balance self.changes = [Memento(self.balance)] self.current = 0 def deposit(self, amount): self.balance += amount m = Memento(self.balance) self.changes.append(m) self.current += 1 return m def restore(self, memento): if memento: self.balance = memento.balance self.changes.append(memento) self.current = len(self.changes)-1 def undo(self): if self.current > 0: self.current -= 1 m = self.changes[self.current] self.balance = m.balance return m return None def redo(self): if self.current + 1 < len(self.changes): self.current += 1 m = self.changes[self.current] self.balance = m.balance return m return None def __str__(self): return f'Balance = {self.balance}' if __name__ == '__main__': ba = BankAccount(100) m50 = ba.deposit(50) ba.deposit(25) print(ba) ba.undo() print(f'Undo 1: {ba}') ba.undo() print(f'Undo 2: {ba}') ba.redo() print(f'Redo 1: {ba}') ba.redo() print(f'Redo 2: {ba}') ba.restore(m50) print(f'Restore: {ba}')
Used when we want to be informed when certain thing happen (e.g. when an object's property changes, when an object does something, external event happen, etc)
The Observer design pattern involves an observer (consumer) component which is an object that wishes to be informed about events happening in the system and one or more observable (producer) components which are the entities generating the events
The observer listens to events and gets a notification when they occur, these notifications usually include useful data, to potentially do something with them. It can also unsubscribe from events if it is no longer interested
class Event(list): def __call__(self, *args, **kwargs): for item in self: item(*args, **kwargs) class Person: def __init__(self, name, address): self.name = name self.address = address self.falls_ill = Event() # Event definition def catch_a_cold(self): self.falls_ill(self.name, self.address) # callback def call_doctor(name, address): print(f'A doctor has been called to {address}') if __name__ == '__main__': person = Person('Sherlock', '221B Baker St') # Subscribe to Event person.falls_ill.append(call_doctor) # callback define in lambda person.falls_ill.append(lambda name, addr: print(f'{name} is ill')) # Event happens person.catch_a_cold() # And you can remove subscriptions too person.falls_ill.remove(call_doctor) person.catch_a_cold()
class Event(list): def __call__(self, *args, **kwargs): for item in self: item(*args, **kwargs) class PropertyObservable: def __init__(self): # this is the event which you need to subscribe self.property_changed = Event() class Person(PropertyObservable): def __init__(self, age=0): super().__init__() self._age = age @property def age(self): return self._age @age.setter def age(self, value): if self._age == value: return self._age = value self.property_changed('age', value) class TrafficAuthority: def __init__(self, person): self.person = person # Subscribe here person.property_changed.append(self.person_changed) # This is the callback def person_changed(self, name, value): if name == 'age': if value < 16: print('Sorry, you still cannot drive') else: print('Okay, you can drive now') # unsubscribe here self.person.property_changed.remove(self.person_changed) if __name__ == '__main__': p = Person() ta = TrafficAuthority(p) for age in range(14, 20): print(f'Setting age to {age}') p.age = age
The State is a pattern in which the object's behavior is determined by its state. This implies the objects is transitioning from one state to another and something needs to trigger a transition
The state design pattern is designed to model cases where the things you can do with a component depend on the state of that component and the changes in state can be explicit, by doing something or implicit or in response to an event
A State Machine is a manager of states and transitions
In the classical OOP implementation of the state design pattern every state is a class and each class handles transitions from one state to another (it is the one that changes the state) however this classical state implementation is not the recommended implementation
from enum import Enum, auto TRIGGER_INDEX = 0 NEXT_STATE_INDEX = 1 TRANSITIONS_KEY = "transitions" FUNC_KEY = "function" class State(Enum): OFF_HOOK = auto() CONNECTING = auto() CONNECTED = auto() ON_HOLD = auto() ON_HOOK = auto() class Trigger(Enum): CALL_DIALED = auto() HUNG_UP = auto() CALL_CONNECTED = auto() PLACED_ON_HOLD = auto() TAKEN_OFF_HOLD = auto() LEFT_MESSAGE = auto() TAKE_OFF_HOOK = auto() def simulate_state_function(*args): print("State action with arguments: {}".format(args)) if __name__ == '__main__': rules = { State.OFF_HOOK: { "transitions": [(Trigger.CALL_DIALED, State.CONNECTING)], "function": simulate_state_function }, State.CONNECTING: { "transitions": [(Trigger.HUNG_UP, State.ON_HOOK), (Trigger.CALL_CONNECTED, State.CONNECTED)], "function": simulate_state_function }, State.CONNECTED: { "transitions": [(Trigger.LEFT_MESSAGE, State.ON_HOOK), (Trigger.HUNG_UP, State.ON_HOOK), (Trigger.PLACED_ON_HOLD, State.ON_HOLD)], "function": simulate_state_function }, State.ON_HOLD: { "transitions": [(Trigger.TAKEN_OFF_HOLD, State.CONNECTED), (Trigger.HUNG_UP, State.ON_HOOK)], "function": simulate_state_function }, State.ON_HOOK: { "transitions": [(Trigger.TAKE_OFF_HOOK, State.OFF_HOOK)], "function": simulate_state_function } } state = State.OFF_HOOK exit_state = State.ON_HOOK # Switch to None to make it an Infinite State Machine while True: print(f'The phone is currently {state}') rules[state][FUNC_KEY](state) if state == exit_state: break for i in range(len(rules[state][TRANSITIONS_KEY])): t = rules[state][TRANSITIONS_KEY][i][TRIGGER_INDEX] print(f'{i}: {t}') idx = int(input('Select a trigger:')) s = rules[state][TRANSITIONS_KEY][idx][NEXT_STATE_INDEX] state = s print('We are done using the phone.')
from enum import Enum, auto class State(Enum): LOCKED = auto() FAILED = auto() UNLOCKED = auto() if __name__ == '__main__': code = '1234' state = State.LOCKED entry = '' while True: if state == State.LOCKED: entry += input("Give me a digit of combination: ") if entry == code: state = State.UNLOCKED if not code.startswith(entry): # the code is wrong state = State.FAILED elif state == State.FAILED: #print('\nFAILED') entry = '' state = State.LOCKED elif state == State.UNLOCKED: print('\nUNLOCKED') break
The Strategy design pattern refers to decomposing a system or algorithm into higher level, which refers to a high level or general description of a process that can be reused and lower level parts, which refers to actual implementation details and specific details of the process
The strategy design pattern comes from the idea that system and algorithms can be decomposed into common parts and specific and enables the exact behavior of a system to be selected at run-time
The idea of the strategy design pattern is that at run-time certain implementation details, that will be feed into a component, are specified and then a high level components consume this and use the low level strategy to actually do something
The strategy is usually a separate chunk of code or component that you can subsequently use inside another object that consumes it to perform an specific operation, the strategy must have a defined interface so that we can switch it
To implement the strategy pattern we rely on composition. First you need to define a high-level or general algorithm, then you define the interface you expect each strategy to follow, these are the methods that will be used in the high-level/general algorithm, finally provide a way for the general algorithm to use the strategies usually through composition, holding a reference(s) to one or more strategies
Yo have an object that takes a strategy object as parameter and uses that strategy methods
from abc import ABC from enum import Enum, auto class OutputFormat(Enum): MARKDOWN = auto() HTML = auto() class ListStrategy(ABC): def start(self, buffer): pass def end(self, buffer): pass def add_list_item(self, buffer, item): pass class MarkdownListStrategy(ListStrategy): def add_list_item(self, buffer, item): buffer.append(f' * {item}\n') class HtmlListStrategy(ListStrategy): def start(self, buffer): buffer.append('
The Template Method design pattern comes from the same idea that algorithms can be decomposed into common parts and specific parts and implements this idea using inheritance
The Template Method allows us to define the skeleton of the algorithm and the concrete implementations details are defined in subclasses
To implement the template method pattern you define the overall algorithm in the base class and use 'abstract members' (methods and properties) which are also defined in the base class, then inheritors override these members providing actual implementations finally the template method gets invoked from the base class and all the overridden members are used to perform the specific operations needed
You have a template method which defines a skeleton algorithm but has a number of blanks which will be defined in the derivatives, you fill the blanks there then call the template method through the derived reference getting all the benefits of the skeleton algorithm and the overridden blanks
from abc import ABC class Game(ABC): def __init__(self, number_of_players): self.number_of_players = number_of_players self.current_player = 0 def run(self): # This is the template method self.start() while not self.have_winner: self.take_turn() print(f'Player {self.winning_player} wins!') def start(self): pass @property def have_winner(self): pass def take_turn(self): pass @property def winning_player(self): pass class Chess(Game): def __init__(self): super().__init__(2) self.max_turns = 10 self.turn = 1 def start(self): print(f'Starting a game of chess with {self.number_of_players} players.') @property def have_winner(self): return self.turn == self.max_turns def take_turn(self): print(f'Turn {self.turn} taken by player {self.current_player}') self.turn += 1 self.current_player = 1 - self.current_player @property def winning_player(self): return self.current_player if __name__ == '__main__': chess = Chess() chess.run() # Call the template method form the derive
The Visitor is a component that knows how to travers a data structure that can be composed of different types, these types can or cannot be related to one another, they could also be in an inheritance tree or some types could contain other types
The visitor is a component that allows you to go over a complicated data structure and do appropriate operation on every single type of node it encounters on the data structure
Used when you need to define a new operation on a entire class hierarchy and don't want to keep modifying every class in the hierarchy, this means avoid explicit type checks
Intrusive Visitor: Modify the actual types that compose a complicated data structure
class DoubleExpression: def __init__(self, value): self.value = value def print(self, buffer): buffer.append(str(self.value)) def eval(self): return self.value class AdditionExpression: def __init__(self, left, right): self.right = right self.left = left def print(self, buffer): buffer.append('(') self.left.print(buffer) buffer.append('+') self.right.print(buffer) buffer.append(')') def eval(self): return self.left.eval() + self.right.eval() if __name__ == '__main__': # represents 1+(2+3) e = AdditionExpression( DoubleExpression(1), AdditionExpression( DoubleExpression(2), DoubleExpression(3) ) ) buffer = [] e.print(buffer) print(''.join(buffer), '=', e.eval()) # breaks OCP: requires we modify the entire hierarchy # what is more likely: new type or new operation?
The Checking of the type is called a reflection operation in some programming languages
from abc import ABC class Expression(ABC): def print(self, buffer): ExpressionPrinter.print(self, buffer) class DoubleExpression(Expression): def __init__(self, value): self.value = value class AdditionExpression(Expression): def __init__(self, left, right): self.right = right self.left = left class ExpressionPrinter: @staticmethod def print(e, buffer): """ Will fail silently on a missing case. """ if isinstance(e, DoubleExpression): buffer.append(str(e.value)) elif isinstance(e, AdditionExpression): buffer.append('(') ExpressionPrinter.print(e.left, buffer) buffer.append('+') ExpressionPrinter.print(e.right, buffer) buffer.append(')') # Equivalent to defined at the ABC level # Expression.print = lambda self, b: ExpressionPrinter.print(self, b) # still breaks OCP because new types require M×N modifications if __name__ == '__main__': # represents 1+(2+3) e = AdditionExpression( DoubleExpression(1), AdditionExpression( DoubleExpression(2), DoubleExpression(3) ) ) buffer = [] # ExpressionPrinter.print(e, buffer) # IDE might complain here e.print(buffer) print(''.join(buffer))
visit
method for each of the types that will be visited, then you call visit()
and the entire structure gets traversed# taken from https://tavianator.com/the-visitor-pattern-in-python/ def _qualname(obj): """Get the fully-qualified name of an object (including module).""" return obj.__module__ + '.' + obj.__qualname__ def _declaring_class(obj): """Get the name of the class that declared an object.""" name = _qualname(obj) return name[:name.rfind('.')] # Stores the actual visitor methods _methods = {} # Delegating visitor implementation def _visitor_impl(self, arg): """Actual visitor method implementation.""" method = _methods[(_qualname(type(self)), type(arg))] return method(self, arg) # The actual @visitor decorator def visitor(arg_type): """Decorator that creates a visitor method.""" def decorator(fn): declaring_class = _declaring_class(fn) _methods[(declaring_class, arg_type)] = fn # Replace all decorated methods with _visitor_impl return _visitor_impl return decorator # ↑↑↑ LIBRARY CODE ↑↑↑ class DoubleExpression: def __init__(self, value): self.value = value class AdditionExpression: def __init__(self, left, right): self.left = left self.right = right class ExpressionPrinter: def __init__(self): self.buffer = [] @visitor(DoubleExpression) def visit(self, de): self.buffer.append(str(de.value)) @visitor(AdditionExpression) def visit(self, ae): self.buffer.append('(') self.visit(ae.left) self.buffer.append('+') self.visit(ae.right) self.buffer.append(')') def __str__(self): return ''.join(self.buffer) class ExpressionEvaluator: def __init__(self): self.value = None @visitor(DoubleExpression) def visit(self, de): self.value = de.value @visitor(AdditionExpression) def visit(self, ae): # ae.left.accept(self) self.visit(ae.left) temp = self.value # need to cache because ExpressionEvaluator is stateful # ae.right.accept(self) self.visit(ae.right) self.value += temp if __name__ == '__main__': # represents 1+(2+3) e = AdditionExpression( DoubleExpression(1), AdditionExpression( DoubleExpression(2), DoubleExpression(3) ) ) printer = ExpressionPrinter() printer.visit(e) evaluator = ExpressionEvaluator() evaluator.visit(e) print(f'{printer} = {evaluator.value}')
If you find the information in this page useful and want to show your support, you can make a donation
Use PayPal
This will help me create more stuff and fix the existent content...