Book Image

Mastering Python Design Patterns - Second Edition

By : Kamon Ayeva, Sakis Kasampalis
Book Image

Mastering Python Design Patterns - Second Edition

By: Kamon Ayeva, Sakis Kasampalis

Overview of this book

Python is an object-oriented scripting language that is used in a wide range of categories. In software engineering, a design pattern is an elected solution for solving software design problems. Although they have been around for a while, design patterns remain one of the top topics in software engineering, and are a ready source for software developers to solve the problems they face on a regular basis. This book takes you through a variety of design patterns and explains them with real-world examples. You will get to grips with low-level details and concepts that show you how to write Python code, without focusing on common solutions as enabled in Java and C++. You'll also fnd sections on corrections, best practices, system architecture, and its designing aspects. This book will help you learn the core concepts of design patterns and the way they can be used to resolve software design problems. You'll focus on most of the Gang of Four (GoF) design patterns, which are used to solve everyday problems, and take your skills to the next level with reactive and functional patterns that help you build resilient, scalable, and robust applications. By the end of the book, you'll be able to effciently address commonly faced problems and develop applications, and also be comfortable working on scalable and maintainable projects of any size.
Table of Contents (17 chapters)

The factory method

The factory method is based on a single function written to handle our object creation task. We execute it, passing a parameter that provides information about what we want, and, as a result, the wanted object is created.

Interestingly, when using the factory method, we are not required to know any details about how the resulting object is implemented and where it is coming from.

Real-world examples

An example of the factory method pattern used in reality is in the context of a plastic toy construction kit. The molding material used to construct plastic toys is the same, but different toys (different figures or shapes) can be produced using the right plastic molds. This is like having a factory method in which the input is the name of the toy that we want (for example, duck or car) and the output (after the molding) is the plastic toy that was requested.

In the software world, the Django web framework uses the factory method pattern for creating the fields of a web form. The forms module, included in Django, supports the creation of different kinds of fields (for example, CharField, EmailField, and so on). And parts of their behavior can be customized using attributes such as max_length or required (j.mp/djangofac). As an illustration, there follows a snippet that a developer could write for a form (the PersonForm form containing the fields name and birth_date) as part of a Django application's UI code:

from django import forms

class PersonForm(forms.Form):
    name = forms.CharField(max_length=100)
    birth_date = forms.DateField(required=False)

Use cases

If you realize that you cannot track the objects created by your application because the code that creates them is in many different places instead of in a single function/method, you should consider using the factory method pattern. The factory method centralizes object creation and tracking your objects becomes much easier. Note that it is absolutely fine to create more than one factory method, and this is how it is typically done in practice. Each factory method logically groups the creation of objects that have similarities. For example, one factory method might be responsible for connecting you to different databases (MySQL, SQLite), another factory method might be responsible for creating the geometrical object that you request (circle, triangle), and so on.

The factory method is also useful when you want to decouple object creation from object usage. We are not coupled/bound to a specific class when creating an object; we just provide partial information about what we want by calling a function. This means that introducing changes to the function is easy and does not require any changes to the code that uses it.

Another use case worth mentioning is related to improving the performance and memory usage of an application. A factory method can improve the performance and memory usage by creating new objects only if it is absolutely necessary. When we create objects using a direct class instantiation, extra memory is allocated every time a new object is created (unless the class uses caching internally, which is usually not the case). We can see that in practice in the following code (file id.py), it creates two instances of the same class, A, and uses the id() function to compare their memory addresses. The addresses are also printed in the output so that we can inspect them. The fact that the memory addresses are different means that two distinct objects are created as follows:

class A:
pass

if __name__ == '__main__':
a = A()
b = A()
print(id(a) == id(b))
print(a, b)

Executing the python id.py command on my computer results in the following output:

False
<__main__.A object at 0x7f5771de8f60> <__main__.A object at 0x7f5771df2208>

Note that the addresses that you see if you execute the file are not the same as the ones I see because they depend on the current memory layout and allocation. But the result must be the same—the two addresses should be different. There's one exception that happens if you write and execute the code in the Python Read-Eval-Print Loop (REPL)—or simply put, the interactive prompt—but that's a REPL-specific optimization which does not happen normally.

Implementing the factory method

Data comes in many forms. There are two main file categories for storing/retrieving data: human-readable files and binary files. Examples of human-readable files are XML, RSS/Atom, YAML, and JSON. Examples of binary files are the .sq3 file format used by SQLite and the .mp3 audio file format used to listen to music.

In this example, we will focus on two popular human-readable formats—XML and JSON. Although human-readable files are generally slower to parse than binary files, they make data exchange, inspection, and modification much easier. For this reason, it is advised that you work with human-readable files, unless there are other restrictions that do not allow it (mainly unacceptable performance and proprietary binary formats).

In this case, we have some input data stored in an XML and a JSON file, and we want to parse them and retrieve some information. At the same time, we want to centralize the client's connection to those (and all future) external services. We will use the factory method to solve this problem. The example focuses only on XML and JSON, but adding support for more services should be straightforward.

First, let's take a look at the data files.

The JSON file, movies.json, is an example (found on GitHub) of a dataset containing information about American movies (title, year, director name, genre, and so on). This is actually a big file but here is an extract, simplified for better readability, to show how its content is organized:

[
{"title":"After Dark in Central Park",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Boarding School Girls' Pajama Parade",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Buffalo Bill's Wild West Parad",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Caught",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Clowns Spinning Hats",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Capture of Boer Battery by British",
"year":1900,
"director":"James H. White", "cast":null, "genre":"Short documentary"},
{"title":"The Enchanted Drawing",
"year":1900,
"director":"J. Stuart Blackton", "cast":null,"genre":null},
{"title":"Family Troubles",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Feeding Sea Lions",
"year":1900,
"director":null, "cast":"Paul Boyton", "genre":null}
]

The XML file, person.xml, is based on a Wikipedia example (j.mp/wikijson), and contains information about individuals (firstName, lastName, gender, and so on) as follows:

  1. We start with the enclosing tag of the persons XML container:
<persons> 
  1. Then, an XML element representing a person's data code is presented as follows:
<person> 
<firstName>John</firstName>
<lastName>Smith</lastName>
<age>25</age>
<address>
<streetAddress>21 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="fax">646 555-4567</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
  1. An XML element representing another person's data is shown by the following code:
<person> 
<firstName>Jimy</firstName>
<lastName>Liar</lastName>
<age>19</age>
<address>
<streetAddress>18 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
  1. An XML element representing a third person's data is shown by the code:
<person> 
<firstName>Patty</firstName>
<lastName>Liar</lastName>
<age>20</age>
<address>
<streetAddress>18 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="mobile">001 452-8819</phoneNumber>
</phoneNumbers>
<gender>
<type>female</type>
</gender>
</person>
  1. Finally, we close the XML container:
</persons>

We will use two libraries that are part of the Python distribution for working with JSON and XML, json and xml.etree.ElementTree, as follows:

import json
import xml.etree.ElementTree as etree

The JSONDataExtractor class parses the JSON file and has a parsed_data() method that returns all data as a dictionary (dict). The property decorator is used to make parsed_data() appear as a normal attribute instead of a method, as follows:

class JSONDataExtractor:
def __init__(self, filepath):
self.data = dict()
with open(filepath, mode='r', encoding='utf-8') as
f:self.data = json.load(f)
@property
def parsed_data(self):
return self.data

The XMLDataExtractor class parses the XML file and has a parsed_data() method that returns all data as a list of xml.etree.Element as follows:

class XMLDataExtractor:
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree

The dataextraction_factory() function is a factory method. It returns an instance of JSONDataExtractor or XMLDataExtractor depending on the extension of the input file path as follows:

def dataextraction_factory(filepath):
if filepath.endswith('json'):
extractor = JSONDataExtractor
elif filepath.endswith('xml'):
extractor = XMLDataExtractor
else:
raise ValueError('Cannot extract data from {}'.format(filepath))
return extractor(filepath)

The extract_data_from() function is a wrapper of dataextraction_factory(). It adds exception handling as follows:

def extract_data_from(filepath):
factory_obj = None
try:
factory_obj = dataextraction_factory(filepath)
except ValueError as e:
print(e)
return factory_obj

The main() function demonstrates how the factory method design pattern can be used. The first part makes sure that exception handling is effective, as follows:

def main():
sqlite_factory = extract_data_from('data/person.sq3')
print()

The next part shows how to work with the JSON files using the factory method. Based on the parsing, the title, year, director name, and genre of the movie can be shown (when the value is not empty), as follows:

json_factory = extract_data_from('data/movies.json')
json_data = json_factory.parsed_data
print(f'Found: {len(json_data)} movies')
for movie in json_data:
print(f"Title: {movie['title']}")
year = movie['year']
if year:
print(f"Year: {year}")
director = movie['director']
if director:
print(f"Director: {director}")
genre = movie['genre']
if genre:
print(f"Genre: {genre}")
print()

The final part shows you how to work with the XML files using the factory method. XPath is used to find all person elements that have the last name Liar (using liars = xml_data.findall(f".//person[lastName='Liar']")). For each matched person, the basic name and phone number information are shown, as follows:

xml_factory = extract_data_from('data/person.xml')
xml_data = xml_factory.parsed_data
liars = xml_data.findall(f".//person[lastName='Liar']")
print(f'found: {len(liars)} persons')
for liar in liars:
firstname = liar.find('firstName').text
print(f'first name: {firstname}')
lastname = liar.find('lastName').text
print(f'last name: {lastname}')
[print(f"phone number ({p.attrib['type']}):", p.text)
for p in liar.find('phoneNumbers')]
print()

Here is the summary of the implementation (you can find the code in the factory_method.py file):

  1. We start by importing the modules we need (json and ElementTree).
  2. We define the JSON data extractor class (JSONDataExtractor).
  3. We define the XML data extractor class (XMLDataExtractor).
  4. We add the factory function, dataextraction_factory(), for getting the right data extractor class.
  5. We also add our wrapper for handling exceptions, the extract_data_from() function.
  6. Finally, we have the main() function, followed by Python's conventional trick for calling it when invoking this file from the command line. The following are the aspects of the main function:
    • We try to extract data from an SQL file (data/person.sq3), to show how the exception is handled
    • We extract data from a JSON file and parse the result
    • We extract data from an XML file and parse the result

The following is the type of output (for the different cases) you will get by calling the python factory_method.py command.

First, there is an exception message when trying to access an SQLite (.sq3) file:

Then, we get the following result from processing the movies file (JSON):

Finally, we get this result from processing the person XML file to find the people whose last name is Liar:

Notice that although JSONDataExtractor and XMLDataExtractor have the same interfaces, what is returned by parsed_data() is not handled in a uniform way. Different Python code must be used to work with each data extractor. Although it would be nice to be able to use the same code for all extractors, this is not realistic for the most part, unless we use some kind of common mapping for the data, which is very often provided by external data providers. Assuming that you can use exactly the same code for handling the XML and JSON files, what changes are required to support a third format, for example, SQLite? Find an SQLite file, or create your own and try it.