4 must-have design patterns in Python

image



Do you write in Python and don’t know which design pattern to start with?

This article discusses popular templates with Python code examples.



Abstract factory



Do not confuse this template with another representative of generative design patterns - the factory method that we examined earlier.

image



The standard Python module of the json library illustrates an example when you want to instantiate objects on behalf of the caller. Consider the JSON line:



text = '{"total": 9.61, "items": ["Americano", "Omelet"]}'
      
      





By default, the json module creates unicode objects for strings like "Americano",

float for 9.61, list for a sequence of elements and dict for keys and values ​​of an object.



But for some, these default settings are not suitable. For example, an accountant is against the json module representing the exact amount of “$ 9 61 cents” as an approximate floating-point number, and would rather use a Decimal instance instead.



This is a concrete example of a problem:





First, write a factory that creates all kinds of objects that the loader uses. Not only the numbers that are being analyzed, but even the container containing them.



 <source lang="python">class Factory(object): def build_sequence(self): return [] def build_number(self, string): return Decimal(string)
      
      





And here is the bootloader that uses this factory:

 class Loader(object): def load(string, factory): sequence = factory.build_sequence() for substring in string.split(','): item = factory.build_number(substring) sequence.append(item) return sequence f = Factory() result = Loader.load('1.23, 4.56', f) print(result)
      
      





 [Decimal('1.23'), Decimal('4.56')]
      
      





Second, separate the specification from the implementation by creating an abstract class. This last step justifies the word “abstract” in the name of the design pattern “Abstract Factory”. Your abstract class ensures that the factory argument to load () is the class corresponding to the required interface:



 from abc import ABCMeta, abstractmethod class AbstractFactory(metaclass=ABCMeta): @abstractmethod def build_sequence(self): pass @abstractmethod def build_number(self, string): pass
      
      





Next, make a concrete Factory with the implementation of the methods the heir to the created abstract class. Factory methods are called with various arguments, which helps to create objects of different types and return them without telling the details to the caller.



Prototype



The Prototype design pattern offers a mechanism by which the caller provides a structure with a class menu to create an instance when a user or other source of dynamic queries selects classes from the selection menu.



image

Easier if no class in the menu needed arguments in __init __ ():



 class Sharp(object): "The symbol ♯." class Flat(object): "The symbol ♭."
      
      





Instead, the Prototype pattern comes into play when creating instances of classes with predefined lists of arguments is required:



 class Note(object): "Musical note 1 Ă· `fraction` measures long." def __init__(self, fraction): self.fraction = fraction
      
      





Pythonic solutions



The pythonic approach will design classes exclusively with positional arguments, without named ones. It is then easy to store the arguments as a tuple, which is provided separately from the class itself. This is a familiar approach to the Thread standard library class, which requests the called target = separately from the passed args = (...). Here are our menu items:



 menu = { 'whole note': (Note, (1,)), 'half note': (Note, (2,)), 'quarter note': (Note, (4,)), 'sharp': (Sharp, ()), 'flat': (Flat, ()), }
      
      





Alternatively, put each class and arguments in one tuple:



 menu = { 'whole note': (Note, 1), 'half note': (Note, 2), 'quarter note': (Note, 4), 'sharp': (Sharp,), 'flat': (Flat,), }
      
      





Then the structure will call each object using some variation of tup [0] (* tup [1:]).



However, the class may need not only positional arguments, but also named ones. In response to this, provide simple callable objects using lambda expressions for classes that require arguments:



 menu = { 'whole note': lambda: Note(fraction=1), 'half note': lambda: Note(fraction=2), 'quarter note': lambda: Note(fraction=4), 'sharp': Sharp, 'flat': Flat, }
      
      





Although lambda expressions do not support fast introspection for validation, they work well if the structure only calls them.



Pattern itself



Now imagine that there are no tuples and the ability to use them as argument lists. First, you will think that you will need factory classes, each of which will remember a specific list of arguments, and then provide these arguments when requesting a new object:



 #    «»: #     . class NoteFactory(object): def __init__(self, fraction): self.fraction = fraction def build(self): return Note(self.fraction) class SharpFactory(object): def build(self): return Sharp() class FlatFactory(object): def build(self): return Flat()
      
      





Fortunately, the situation is not so gloomy. If you re-read the factory classes above, you will notice that each of them is surprisingly similar to the target classes that we want to create. Like Note, NoteFactory itself stores the fraction attribute. The factory stack looks, at a minimum, with attribute lists, like a stack of target classes being created.



This symmetry offers a way to solve our problem without mirroring each class using a factory. What if we used the original objects themselves to store the arguments and enabled them to provide new instances?



The result will be the Prototype pattern, which we will write in Python from scratch. All factory classes disappear. Instead, each object has a clone () method, to the call of which it responds by creating a new instance with the arguments received:



 #  «»:    #     . class Note(object): "Musical note 1 ÷ `fraction` measures long." def __init__(self, fraction): self.fraction = fraction def clone(self): return Note(self.fraction) class Sharp(object): "The symbol ♯." def clone(self): return Sharp() class Flat(object): "The symbol ♭." def clone(self): return Flat()
      
      





Although the example already illustrates the design pattern, complicate it if you wish. For example, add a type (self) call in each clone () method instead of hard-coding the name of your own class for the case of calling a method in a subclass.



Linker



The “Linker” pattern assumes that when developing “container” objects that collect and organize “content objects”, you simplify operations if you provide containers and content objects with a common set of methods. And thus, you maintain the maximum possible methods, despite the fact that it does not matter to the caller that a separate content object or an entire container is transferred.



image



Implementation: inherit or not?



The benefits of the symmetry this pattern creates between containers and their contents only increase if the symmetry makes objects interchangeable. But here, some statically typed languages ​​encounter an obstacle.



In strongly typed languages, objects of two classes are interchangeable only when inheriting from one parent class that implements common methods, or when inheriting one class directly from another.



In other static languages, the restriction is milder. There is no strict need for the container and its contents to share the implementation. As long as both correspond to an “interface” that declares specific generic methods, objects are called symmetrically.



Since this is Python programming, both restrictions evaporate! Write code in your preferred range of security and brevity. If you want, go the classic way and add a general superclass:



 class Widget(object): def children(self): return [] class Frame(Widget): def __init__(self, child_widgets): self.child_widgets = child_widgets def children(self): return self.child_widgets class Label(Widget): def __init__(self, text): self.text = text
      
      





Or give the objects the same interface. And rely on tests to help maintain symmetry between containers and content. (Where, for simple scripts, your “test” may be a fact of code execution.)



 class Frame(object): def __init__(self, child_widgets): self.child_widgets = child_widgets def children(self): return self.child_widgets class Label(object): def __init__(self, text): self.text = text def children(self): return []
      
      







Since Python offers such a range of approaches, you should not define the “Linker” pattern classically, that is, as one specific mechanism (superclass) to create or guarantee symmetry. Instead, define it as creating symmetry by any means in the hierarchy of objects.



Iterator



How to implement the Iterator design pattern and connect to the built-in iterative mechanisms of the Python language for, iter () and next ()?



image



Add the __iter __ () method to the container, which returns an iterator object. Support for this method makes the container iterable.



For each iterator, set the __next __ () method (in the old Python 2 code, next () was written without double underscore), which returns the next element from the container with each call. Throw StopIterator exception when there are no more elements.



Remember that some users pass iterators to the for loop instead of the main container? To be safe in this case, each iterator also needs the __iter __ () method, which returns itself.



See how these requirements work together, using our own iterator as an example!



Please note that the elements resulting from __next __ () are not required to be stored as constant values ​​inside the container or even present before __next __ () is called. So you can write an example of the Iterator design pattern even without implementing storage in the container:



 class OddNumbers(object): "An iterable object." def __init__(self, maximum): self.maximum = maximum def __iter__(self): return OddIterator(self) class OddIterator(object): "An iterator." def __init__(self, container): self.container = container self.n = -1 def __next__(self): self.n += 2 if self.n > self.container.maximum: raise StopIteration return self.n def __iter__(self): return self
      
      





Thanks to these three methods - one for the container object and two for its iterator - the OddNumbers container is now fully involved in the rich iterative ecosystem of the Python programming language. It will work without problems with the for loop:

 numbers = OddNumbers(7) for n in numbers: print(n)
      
      





 1 3 5 7
      
      





And it also works with the built-in methods iter () and next ().

 it = iter(OddNumbers(5)) print(next(it)) print(next(it))
      
      





 1 3
      
      





He is friends even with generators of lists and sets!



 print(list(numbers)) print(set(n for n in numbers if n > 4))
      
      





 [1, 3, 5, 7] {5, 7}
      
      





Three simple methods - and you have unlocked access to iteration support at the Python syntax level.



Summary



You will now make friends with Python with a pair of generative design patterns — an abstract factory and a prototype. Easily implement a structural template - a prototype. You can handle the implementation of a behavioral design pattern - an iterator.



All Articles