본문 바로가기
Mastering Python Design Patterns

Other Behavioral Patterns

by 자동매매 2023. 3. 22.

Other Behavioral Patterns

We have seen in Chapter 12, *The State pattern, which helps us in making behavior changes when an object's internal state changes, by using state machines. There is a number of behavioral patterns and, in this chapter, we are going to discuss five more of them: Interpreter, Strategy, Memento, Iterator, and Template.

What is the Interpreter pattern? The Interpreter pattern is interesting for the advanced users of an application. The main idea behind this pattern is to give the ability to non-beginner users and domain experts to use a simple language, to get more productive in doing what they need to with the application.

What is the Strategy pattern? The Strategy pattern promotes using multiple algorithms to solve a problem. For example, if you have two algorithms to solve a problem with some difference in performance depending on the input data, you can use strategy to decide which algorithm to use based on the input data at runtime.

What is the Memento pattern? The Memento pattern helps add support for Undo and/or History in an application. When implemented, for a given object, the user is able to restore a previous state that was created and kept for later possible use.

What is the Iterator pattern? The Iterator pattern offers an efficient way to handle a container of objects and traverse to these members one at a time, using the famous next semantic. It is really useful since, in programming, we use sequences and collections of objects a lot, particularly in algorithms.

What is the Template pattern? The Template pattern focuses on eliminating code redundancy. The idea is that we should be able to redefine certain parts of an algorithm without changing its structure.

Other Behavioral Patterns Chapter 178*

In this chapter, we will discuss the following:

  • The Interpreter pattern
    • The Strategy pattern
      • The Memento pattern
        • The Iterator pattern
          • The Template pattern

Interpreter pattern

Usually, what we want to create is a domain-specific language (DSL). A DSL is a computer language of limited expressiveness targeting a particular domain. DSLs are used for different things, such as combat simulation, billing, visualization, configuration, communication protocols, and so on. DSLs are divided into internal DSLs and external DSLs, j.mp/wikidsl, and j.mp/fowlerdsl.

Internal DSLs are built on top of a host programming language. An example of an internal DSL is a language that solves linear equations using Python. The advantages of using an internal DSL are that we don't have to worry about creating, compiling, and parsing grammar because these are already taken care of by the host language. The disadvantage is that we are constrained by the features of the host language. It is very challenging to create an expressive, concise, and fluent internal DSL if the host language does not have these features j.mp/jwodsl.

External DSLs do not depend on host languages. The creator of the DSL can decide all aspects of the language (grammar, syntax, and so forth), but they are also responsible for creating a parser and compiler for it. Creating a parser and compiler for a new language can be a very complex, long, and painful procedure j.mp/jwodsl.

The interpreter pattern is related only to internal DSLs. Therefore, our goal is to create a simple but useful language using the features provided by the host programming language, which in this case is Python. Note that Interpreter does not address parsing at all. It assumes that we already have the parsed data in some convenient form. This can be an abstract syntax tree (AST) or any other handy data structure [Gang of Four-95 book, page 276].

Real-world examples

A musician is an example of the Interpreter pattern in reality. Musical notation represents the pitch and duration of a sound graphically. The musician is able to reproduce a sound precisely based on its notation. In a sense, musical notation is the language of music, and the musician is the interpreter of that language.

We can also cite software examples:

  • In the C++ world, boost::spirit is considered an internal DSL for implementing parsers.
    • An example in Python is PyT, an internal DSL to generate (X)HTML. PyT focuses on performance and claims to have comparable speed with Jinja2 j.mp/ghpyt. Of course, we should not assume that the Interpreter pattern is necessarily used in PyT. However, since it is an internal DSL, the Interpreter is a very good candidate for it.

Use cases

The Interpreter pattern is used when we want to offer a simple language to domain experts and advanced users to solve their problems. The first thing we should stress is that the interpreter should only be used to implement simple languages. If the language has the requirements of an external DSL, there are better tools to create languages from scratch (Yacc and Lex, Bison, ANTLR, and so on).

Our goal is to offer the right programming abstractions to the specialist, who is often not a programmer, to make them productive. Ideally, they shouldn't know advanced Python to use our DSL, but knowing even a little bit of Python is a plus since that's what we eventually get at the end. Advanced Python concepts should not be a requirement. Moreover, the performance of the DSL is usually not an important concern. The focus is on offering a language that hides the peculiarities of the host language and offers a more human-readable syntax. Admittedly, Python is already a very readable language with far less peculiar syntax than many other programming languages.

Implementation

Let's create an internal DSL to control a smart house. This example fits well into the Internet of Things (IoT) era, which is getting more and more attention nowadays. The user is able to control their home using a very simple event notation. An event has the form of command -> receiver -> arguments. The arguments part is optional.

Not all events require arguments. An example of an event that does not require any arguments is shown here:

open -> gate

An example of an event that requires arguments is shown here:

increase -> boiler temperature -> 3 degrees

The -> symbol is used to mark the end of one part of an event and state the beginning of the next one. There are many ways to implement an internal DSL. We can use plain old regular expressions, string processing, a combination of operator overloading, and metaprogramming, or a library/tool that can do the hard work for us. Although officially, the interpreter does not address parsing, I feel that a practical example needs to cover parsing as well. For this reason, I decided to use a tool to take care of the parsing part. The tool is called Pyparsing and, to find out more about it, check the mini-book Getting Started with Pyparsing by Paul McGuire. If Pyparsing is not already installed on your system, you can install it using the pip install pyparsing command.

Before getting into coding, it is a good practice to define a simple grammar for our language. We can define the grammar using the Backus-Naur Form (BNF) notation [j.mp/bnfgram]:

event ::= command token receiver token arguments

command ::= word+

word ::= a collection of one or more alphanumeric characters token ::= ->

receiver ::= word+

arguments ::= word+

What the grammar basically tells us is that an event has the form of command ->

receiver -> arguments, and that commands, receivers, and arguments have the same form: a group of one or more alphanumeric characters. If you are wondering about the necessity of the numeric part, it is included to allow us to pass arguments, such as three degrees at the increase -> boiler temperature -> 3 degrees command.

Now that we have defined the grammar, we can move on to converting it to actual code. Here's how the code looks:

word = Word(alphanums)

command = Group(OneOrMore(word))

token = Suppress("->")

device = Group(OneOrMore(word))

argument = Group(OneOrMore(word))

event = command + token + device + Optional(token + argument)

The basic difference between the code and grammar definition is that the code needs to be written in the bottom-up approach. For instance, we cannot use a word without first assigning it a value. Suppress is used to state that we want the -> symbol to be skipped from the parsed results.

The full code of this implementation example (the interpreter.py file) uses many placeholder classes, but to keep you focused, I will first show a minimal version featuring only one class. Let's take a look at the Boiler class. A boiler has a default temperature of 83° Celsius. There are also two methods to increase and decrease the current temperature:

class Boiler:

def __init__(self):

self.temperature = 83 # in celsius

def __str__(self):

return f'boiler temperature: {self.temperature}'

def increase_temperature(self, amount):

print(f"increasing the boiler's temperature by {amount} degrees") self.temperature += amount

def decrease_temperature(self, amount):

print(f"decreasing the boiler's temperature by {amount} degrees") self.temperature -= amount

The next step is to add the grammar, which we already covered. We will also create a boiler instance and print its default state:

word = Word(alphanums)

command = Group(OneOrMore(word))

token = Suppress("->")

device = Group(OneOrMore(word))

argument = Group(OneOrMore(word))

event = command + token + device + Optional(token + argument)

boiler = Boiler() print(boiler)

The simplest way to retrieve the parsed output of pyparsing is by using the parseString() method. The result is a ParseResults instance, which is actually a parse tree that can be treated as a nested list. For example, executing print(event.parseString('increase -> boiler temperature -> 3 degrees'))

would give [['increase'], ['boiler', 'temperature'], ['3', 'degrees']] as a result.

So, in this case, we know that the first sublist is the command (increase), the second sublist is the receiver (boiler temperature), and the third sublist is the argument (3°). We can actually unpack the ParseResults instance, which gives us direct access to these three

parts of the event. Having direct access means that we can match patterns to find out which method should be executed:

cmd, dev, arg = event.parseString('increase -> boiler temperature -> 3 degrees')

cmd_str = ' '.join(cmd)

dev_str = ' '.join(dev)

if 'increase' in cmd_str and 'boiler' in dev_str: boiler.increase_temperature(int(arg[0])) print(boiler)

Executing the preceding code snippet (using python boiler.py, as usual) gives the following output:

The full code (the interpreter.py file) is not very different from what I just described. It is just extended to support more events and devices. Let's present it now:

First, we import all we need from pyparsing:

from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums

We define the Gate class:

class Gate:

def __init__(self):

self.is_open = False

def __str__(self):

return 'open' if self.is_open else 'closed' def open(self):

print('opening the gate')

self.is_open = True

def close(self):

print('closing the gate')

self.is_open = False

  • We define the Garage class:

class Garage:

def __init__(self):

self.is_open = False

def __str__(self):

return 'open' if self.is_open else 'closed' def open(self):

print('opening the garage')

self.is_open = True

def close(self):

print('closing the garage')

self.is_open = False

  • We define the Aircondition class:

class Aircondition:

def __init__(self):

self.is_on = False

def __str__(self):

return 'on' if self.is_on else 'off'

def turn_on(self):

print('turning on the air condition') self.is_on = True

def turn_off(self):

print('turning off the air condition') self.is_on = False

We also introduce the Heating class:

class Heating:

def __init__(self):

self.is_on = False

def __str__(self):

return 'on' if self.is_on else 'off' def turn_on(self):

print('turning on the heating')

self.is_on = True

def turn_off(self):

print('turning off the heating')

self.is_on = False

We define the Boiler class, already presented:

class Boiler:

def __init__(self):

self.temperature = 83 # in celsius def __str__(self):

return f'boiler temperature: {self.temperature}'

def increase_temperature(self, amount):

print(f"increasing the boiler's temperature by {amount} degrees")

self.temperature += amount

def decrease_temperature(self, amount):

print(f"decreasing the boiler's temperature by {amount} degrees")

self.temperature -= amount

  • Lastly, we define the Fridge class:

class Fridge:

def __init__(self):

self.temperature = 2 # in celsius

def __str__(self):

return f'fridge temperature: {self.temperature}'

def increase_temperature(self, amount):

print(f"increasing the fridge's temperature by {amount} degrees")

self.temperature += amount

def decrease_temperature(self, amount):

print(f"decreasing the fridge's temperature by {amount} degrees")

self.temperature -= amount

  • Next, let's see our main function, starting with its first part:

def main():

word = Word(alphanums)

command = Group(OneOrMore(word))

token = Suppress("->")

device = Group(OneOrMore(word))

argument = Group(OneOrMore(word))

event = command + token + device + Optional(token + argument) gate = Gate()

garage = Garage()

airco = Aircondition()

heating = Heating()

boiler = Boiler()

fridge = Fridge()

  • We prepare the parameters for tests we will be performing, using the following variables (tests, open_actions, and close_actions):

tests = ('open -> gate',

'close -> garage',

'turn on -> air condition',

'turn off -> heating',

'increase -> boiler temperature -> 5 degrees', 'decrease -> fridge temperature -> 2 degrees')

open_actions = {'gate':gate.open, 'garage':garage.open,

'air condition':airco.turn_on, 'heating':heating.turn_on, 'boiler temperature':boiler.increase_temperature, 'fridge temperature':fridge.increase_temperature}

close_actions = {'gate':gate.close, 'garage':garage.close,

'air condition':airco.turn_off, 'heating':heating.turn_off, 'boiler temperature':boiler.decrease_temperature, 'fridge temperature':fridge.decrease_temperature}

  • Now, we execute the test actions, using the following snippet (that ends the main function):

for t in tests:

if len(event.parseString(t)) == 2: # no argument

cmd, dev = event.parseString(t)

cmd_str, dev_str = ' '.join(cmd), ' '.join(dev)

if 'open' in cmd_str or 'turn on' in cmd_str: open_actionsdev_str

elif 'close' in cmd_str or 'turn off' in cmd_str: close_actionsdev_str

elif len(event.parseString(t)) == 3: # argument

cmd, dev, arg = event.parseString(t)

cmd_str, dev_str, arg_str = ' '.join(cmd), '

'.join(dev), ' '.join(arg)

num_arg = 0

try:

num_arg = int(arg_str.split()[0]) # extract the numeric part

except ValueError as err:

print(f"expected number but got: '{arg_str[0]}'") if 'increase' in cmd_str and num_arg > 0: open_actionsdev_str

elif 'decrease' in cmd_str and num_arg > 0: close_actionsdev_str

  • We add the snippet to call the main function:

if __name__ == '__main__':

main()

Executing the python interpreter.py command gives the following output:

If you want to experiment more with this example, I have a few suggestions for you. The first change that will make it much more interesting is to make it interactive. Currently, all the events are hardcoded in the tests tuple. However, the user wants to be able to activate events using an interactive prompt. Do not forget to check how sensitive pyparsing is regarding spaces, tabs, or unexpected input. For example, what happens if the user types turn off -> heating 37?

Strategy pattern

Most problems can be solved in more than one way. Take, for example, the sorting problem, which is related to putting the elements of a list in a specific order.

There are many sorting algorithms, and, in general, none of them is considered the best for all cases j.mp/algocomp.

There are different criteria that help us pick a sorting algorithm on a per-case basis. Some of the things that should be taken into account are listed here:

  • A number of elements that need to be sorted: This is called the input size. Almost all the sorting algorithms behave fairly well when the input size is small, but only a few of them have good performance with a large input size.
    • The best/average/worst time complexity of the algorithm: Time complexity is (roughly) the amount of time the algorithm takes to complete, excluding coefficients and lower-order terms. This is often the most usual criterion to pick an algorithm, although it is not always sufficient.
  • The space complexity of the algorithm: Space complexity is (again roughly) the amount of physical memory needed to fully execute an algorithm. This is very important when we are working with big data or embedded systems, which usually have limited memory.
    • Stability of the algorithm: An algorithm is considered stable when it maintains the relative order of elements with equal values after it is executed.
      • Code complexity of the algorithm: If two algorithms have the same time/space complexity and are both stable, it is important to know which algorithm is easier to code and maintain.

There are possibly more criteria that can be taken into account. The important question is are we really forced to use a single sorting algorithm for all cases? The answer is of course not. A better solution is to have all the sorting algorithms available and using the mentioned criteria to pick the best algorithm for the current case. That's what the Strategy pattern is about.

The Strategy pattern promotes using multiple algorithms to solve a problem. Its killer feature is that it makes it possible to switch algorithms at runtime transparently (the client code is unaware of the change). So, if you have two algorithms and you know that one works better with small input sizes, while the other works better with large input sizes, you can use Strategy to decide which algorithm to use based on the input data at runtime.

Real-world examples

Reaching an airport to catch a flight is a good Strategy example used in real life:

  • If we want to save money and we leave early, we can go by bus/train
    • If we don't mind paying for a parking place and have our own car, we can go by car
      • If we don't have a car but we are in a hurry, we can take a taxi

There are trade-offs between cost, time, convenience, and so forth.

In software, Python's sorted() and list.sort() functions are examples of the Strategy pattern. Both functions accept a named parameter key, which is basically the name of the function that implements a sorting strategy [Python 3 Patterns, Recipes, and Idioms, by Bruce Eckel-08, page 202].

Use cases

Strategy is a very generic design pattern with many use cases. In general, whenever we want to be able to apply different algorithms dynamically and transparently, Strategy is the way to go. By different algorithms, I mean different implementations of the same algorithm. This means that the result should be exactly the same, but each implementation has a different performance and code complexity (as an example, think of sequential search versus binary search).

We have already seen how Python and Java use the Strategy pattern to support different sorting algorithms. However, Strategy is not limited to sorting. It can also be used to create all kinds of different resource filters (authentication, logging, data compression, encryption, and so forth) [j.mp/javaxfilter].

Another usage of the Strategy pattern is to create different formatting representations, either to achieve portability (for example, line-breaking differences between platforms) or dynamically change the representation of data.

Implementation

There is not much to be said about implementing the Strategy pattern. In languages where functions are not first-class citizens, each Strategy should be implemented in a different class. Wikipedia demonstrates that at j.mp/stratwiki. In Python, we can treat functions as normal variables and this simplifies the implementation of Strategy.

Assume that we are asked to implement an algorithm to check if all characters in a string are unique. For example, the algorithm should return true if we enter the dream string because none of the characters are repeated. If we enter the pizza string, it should return false because the letter z exists two times. Note that the repeated characters do not need to be consecutive, and the string does not need to be a valid word. The algorithm should also return false for the 1r2a3ae string, because the letter a appears twice.

After thinking about the problem carefully, we come up with an implementation that sorts the string and compares all characters pair by pair. First, we implement the pairs() function, which returns all neighbor pairs of a sequence, seq:

def pairs(seq):

n = len(seq)

for i in range(n):

yield seq[i], seq[(i + 1) % n]

Next, we implement the allUniqueSort() function, which accepts a string, s, and returns True if all characters in the string are unique; otherwise, it returns False. To demonstrate the Strategy pattern, we will make a simplification by assuming that this algorithm fails to scale. We assume that it works fine for strings that are up to five characters. For longer strings, we simulate a slowdown by inserting a sleep statement:

SLOW = 3 # in seconds

LIMIT = 5 # in characters WARNING = 'too bad, you picked the slow algorithm :(' def allUniqueSort(s):

if len(s) > LIMIT:

print(WARNING)

time.sleep(SLOW)

srtStr = sorted(s)

for (c1, c2) in pairs(srtStr):

if c1 == c2:

return False

return True

We are not happy with the performance of allUniqueSort(), and we are trying to think

of ways to improve it. After some time, we come up with a new algorithm,

allUniqueSet(), that eliminates the need to sort. In this case, we use a set. If the character

in check has already been inserted in the set, it means that not all characters in the string are unique:

def allUniqueSet(s):

if len(s) < LIMIT:

print(WARNING)

time.sleep(SLOW)

return True if len(set(s)) == len(s) else False

Unfortunately, while allUniqueSet() has no scaling problems, for some strange reason, it

has worse performance than allUniqueSort() when checking short strings. What can we

do in this case? Well, we can keep both algorithms and use the one that fits best, depending on the length of the string that we want to check.

The allUnique() function accepts an input string, s, and a strategy function, strategy, which in this case is one of allUniqueSort(), allUniqueSet(). The allUnique() function executes the input strategy and returns its result to the caller.

Then, the main() function lets the user perform the following:

  • Enter the word to be checked for character uniqueness
    • Choose the pattern that will be used

It also does some basic error handling and gives the ability to the user to quit gracefully:

def main():

while True:

word = None

while not word:

word = input('Insert word (type quit to exit)> ')

if word == 'quit':

print('bye')

return

strategy_picked = None

strategies = { '1': allUniqueSet, '2': allUniqueSort }

while strategy_picked not in strategies.keys():

strategy_picked = input('Choose strategy: [1] Use a set, [2] Sort and pair> ') try:

strategy = strategies[strategy_picked]

print(f'allUnique({word}): {allUnique(word, strategy)}')

except KeyError as err:

print(f'Incorrect option: {strategy_picked}')

Here's the complete code of the example (the strategy.py file):

We import the time module:

import time

We define the pairs() function:

def pairs(seq):

n = len(seq)

for i in range(n):

yield seq[i], seq[(i + 1) % n]

We define the values for the SLOW, LIMIT , and WARNING constants:

SLOW = 3 # in seconds

LIMIT = 5 # in characters WARNING = 'too bad, you picked the slow algorithm :('

  • We define the function for the first algorithm, allUniqueSort():

def allUniqueSort(s):

if len(s) > LIMIT: print(WARNING) time.sleep(SLOW)

srtStr = sorted(s)

for (c1, c2) in pairs(srtStr): if c1 == c2:

return False

return True

  • We define the function for the second algorithm, allUniqueSet():

def allUniqueSet(s):

if len(s) < LIMIT:

print(WARNING)

time.sleep(SLOW)

return True if len(set(s)) == len(s) else False

Next, we define the allUnique() function that helps call a chosen algorithm by passing the corresponding strategy function:

def allUnique(word, strategy): return strategy(word)

Now is the time for the main() function, followed by Python's script execution stanza:

def main():

while True:

word = None

while not word:

word = input('Insert word (type quit to exit)> ')

if word == 'quit':

print('bye')

return

strategy_picked = None

strategies = { '1': allUniqueSet, '2': allUniqueSort } while strategy_picked not in strategies.keys():

strategy_picked = input('Choose strategy: [1] Use a set, [2] Sort and pair> ')

try:

strategy = strategies[strategy_picked]

print(f'allUnique({word}): {allUnique(word, strategy)}')

except KeyError as err:

print(f'Incorrect option: {strategy_picked}')

if __name__ == "__main__":

main()

Let's see the output of a sample execution using the python strategy.py command:

The first word, balloon, has more than five characters and not all of them are unique. In this case, both algorithms return the correct result, False, but allUniqueSort() is slower and the user is warned.

The second word, bye, has less than five characters and all characters are unique. Again, both algorithms return the expected result, True, but this time, allUniqueSet() is slower and the user is warned once more.

Normally, the strategy that we want to use should not be picked by the user. The point of the Strategy pattern is that it makes it possible to use different algorithms transparently. Change the code so that the faster algorithm is always picked.

There are two usual users of our code. One is the end user, who should be unaware of what's happening in the code, and to achieve that we can follow the tips given in the previous paragraph. Another possible category of users is the other developers. Assume that we want to create an API that will be used by the other developers. How can we keep them unaware of the Strategy pattern? A tip is to think of encapsulating the two functions in a common class, for example, allUnique. In this case, the other developers will just

need to create an instance of allUnique and execute a single method, for instance, test(). What needs to be done in this method?

Memento pattern

In many situations, we need a way to easily take a snapshot of the internal state of an object, so that we can restore the object with it when needed. Memento is a design pattern that can help us implement a solution for such situations.

The Memento design pattern has three key components:

  • Memento, a simple object that contains basic state storage and retrieval capabilities
    • Originator, an object that gets and sets values of Memento instances
      • Caretaker, an object that can store and retrieve all previously created Memento instances

Memento shares many similarities with the Command pattern.

Real-world examples

The memento pattern can be seen in many situations in real life.

An example could be found in the dictionary we use for a language, such as English or French. The dictionary is regularly updated through the work of the academic experts, with new words being added and other words becoming obsolete. Spoken and written languages evolve and the official dictionary has to reflect that. From time to time, we revisit a previous edition to get an understanding of how the language was used at some point in the past. This could also be needed simply because information can be lost after a long period of time, and to find it, you may need to look into old editions. That can be useful to understand something in a particular field. Someone doing a research could use an old dictionary or go to the archives to find information about some words and expressions.

This example can be extended to other written material, such as books and newspapers.

Zope (http:/ / www.zope.org), with its integrated object database, called Zope Object

Database (ZODB), offers a good software example of the Memento pattern. It is famous for its object, Undo support, exposed Through The Web for content managers (website administrators). ZODB is an object database for Python and is in heavy use in the Pyramid and Plone communities, and in many other applications.

Use cases

Memento is usually used when you need to provide some sort of undo and redo capability for your users.

Another usage is the implementation of a UI dialog with Ok/Cancel buttons, where we would store the state of the object on load, and if the user chooses to cancel, we would restore the initial state of the object.

Implementation

We will approach the implementation of Memento, in a simplified way, and by doing things in a natural way for the Python language. This means we do not necessarily need several classes.

One thing we will use is Python's pickle module. What is pickle used for? According to the module's documentation (https:/ / docs.python.org/3/ library/pickle.html), we can see that the pickle module can transform a complex object into a byte stream and it can transform the byte stream into an object with the same internal structure.

Let's take a Quote class, with the attributes text and author. To create the memento, we will use a method on that class, save_state(), which as the name suggests will dump the state of the object, using the pickle.dumps() function. This creates the memento:

class Quote:

def __init__(self, text, author): self.text = text

self.author = author

def save_state(self):

current_state = pickle.dumps(self.__dict__) return current_state

That state can be restored later. For that, we add the restore_state() method, making use of the pickle.loads() function:

def restore_state(self, memento):

previous_state = pickle.loads(memento) self.__dict__.clear() self.__dict__.update(previous_state)

Let's also add the __str__ method:

def __str__(self):

return f'{self.text} - By {self.author}.'

Then, in the main function, we can take care of things and test our implementation, as usual:

def main():

print('Quote 1')

q1 = Quote("A room without books is like a body without a soul.", 'Unknown author')

print(f'\nOriginal version:\n{q1}')

q1_mem = q1.save_state()

q1.author = 'Marcus Tullius Cicero'

print(f'\nWe found the author, and did an updated:\n{q1}')

q1.restore_state(q1_mem)

print(f'\nWe had to restore the previous version:\n{q1}')

print()

print('Quote 2')

q2 = Quote("To be you in a world that is constantly trying to make you be something else is the greatest accomplishment.",

'Ralph Waldo Emerson')

print(f'\nOriginal version:\n{q2}')

q2_mem1 = q2.save_state()

q2.text = "To be yourself in a world that is constantly trying to make you something else is the greatest accomplishment."

print(f'\nWe fixed the text:\n{q2}')

q2_mem2 = q2.save_state()

q2.text = "To be yourself when the world is constantly trying to make you something else is the greatest accomplishment."

print(f'\nWe fixed the text again:\n{q2}') q2.restore_state(q2_mem2)

print(f'\nWe had to restore the 2nd version, the correct one:\n{q2}')

Here's the complete code of the example (the memento.py file):

  • We import the pickle module:

import pickle

  • We define the Quote class:

class Quote:

def __init__(self, text, author): self.text = text

self.author = author

def save_state(self):

current_state = pickle.dumps(self.__dict__) return current_state

def restore_state(self, memento):

previous_state = pickle.loads(memento) self.__dict__.clear() self.__dict__.update(previous_state)

def __str__(self):

return f'{self.text} - By {self.author}.'

  • Here is our main function:

def main():

print('Quote 1')

q1 = Quote("A room without books is like a body without a soul.",

'Unknown author')

print(f'\nOriginal version:\n{q1}')

q1_mem = q1.save_state()

  • Now, we found the author's name

q1.author = 'Marcus Tullius Cicero'

print(f'\nWe found the author, and did an updated:\n{q1}')

  • Restoring previous state (Undo)

q1.restore_state(q1_mem)

print(f'\nWe had to restore the previous version:\n{q1}')

print()

print('Quote 2')

q2 = Quote("To be you in a world that is constantly trying to make you be something else is the greatest accomplishment.",

'Ralph Waldo Emerson')

print(f'\nOriginal version:\n{q2}')

q2_mem1 = q2.save_state()

  • changes to the text

q2.text = "To be yourself in a world that is constantly trying

to make you something else is the greatest accomplishment."

print(f'\nWe fixed the text:\n{q2}')

q2_mem2 = q2.save_state()

q2.text = "To be yourself when the world is constantly trying to make you something else is the greatest accomplishment."

print(f'\nWe fixed the text again:\n{q2}')

  • Restoring previous state (Undo)

q2.restore_state(q2_mem2)

print(f'\nWe had to restore the 2nd version, the correct one:\n{q2}')

  • Let's not forget the script execution stanza:

if __name__ == "__main__":

main()

Let's view a sample execution using the python memento.py command:

The output shows the program does what we expected: we can restore a previous state for each of our Quote objects.

Iterator pattern

In programming, we use sequences or collections of objects a lot, particularly in algorithms and when writing programs that manipulate data. One can think of automation scripts, APIs, data-driven apps, and other domains. In this chapter, we are going to see a pattern that is really useful whenever we have to handle collections of objects: the Iterator pattern.

Note the following, according to the definition given by Wikipedia:

Iterator is a design pattern in which an iterator is used to traverse a container and access the container's elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled.

The iterator pattern is extensively used in the Python context. As we will see, this translates into iterator being a language feature. It is so useful that the language developers decided to make it a feature.

Real-world examples

Whenever you have a collection of things, and you have to go through the collection by taking those things one by one, it is an example of iterator pattern.

So, there are many examples in life, for instance as follows:

  • A classroom where the teacher is going to each student to give them their textbook
    • A waiter in a restaurant attending to people at a table, and taking the order of each person

What about software?

As we said, iteration is already in Python as a feature. We have iterable objects and iterators. Container or sequence types (list, tuple, string, dictionary, set, and so on) are iterable, meaning we can iterate through them. Iteration is done automatically for you whenever you use the for or while loop to traverse those objects and access their members.

But, we are not limited to that case. Python also has the built-in iter() function, which is a helper to transform any object in an iterator.

Use cases

It is a good idea to use the iterator pattern whenever you want one or several of the following behaviors:

  • Make it easy to navigate through a collection
    • Get the next object in the collection at any point
      • Stop when you are done traversing through the collection

Implementation

Iterator is implemented in Python for us, within for loops, list comprehensions, and so on. Iterator in Python is simply an object that can be iterated upon. An object which will return data, one element at a time.

We can do our own implementation for special cases, using the Iterator protocol, meaning that our iterator object must implement two special methods: __iter__() and

__next__().

An object is called iterable if we can get an iterator from it. Most of the built-in containers in Python (list, tuple, set, string, and so on) are iterable. The iter() function (which in turn calls the __iter__() method) returns an iterator from them.

Let's consider a football team we want to implement with the help of the FootballTeam class. If we want to make an iterator out of it, we have to implement the iterator protocol, since it is not a built-in container type such as the list type. Basically, built-in iter() and next() functions would not work on it unless they are added to the implementation.

First, we define the class of the iterator (FootballTeamIterator) that will be used to iterate through the football team object. The members attribute allows us to initialize the iterator object with our container object (which will be a FootballTeam instance):

class FootballTeamIterator:

def __init__(self, members): self.members = members

self.index = 0

We add a __iter__() method to it, which would return the object itself, and a

__next__() method to return the next person from the team at each call until we reach the last person. These will allow looping over the members of the football team via the iterator:

def __iter__(self):

return self

def __next__(self):

if self.index < len(self.members): val = self.members[self.index] self.index += 1

return val

else:

raise StopIteration()

So, now for the FootballTeam class itself; the new thing is adding a __iter__() method to it that will initialize the iterator object that it needs (thus using FootballTeamIterator(self.members)) and return it:

class FootballTeam:

def __init__(self, members):

self.members = members

def __iter__(self):

return FootballTeamIterator(self.members)

We add a small main function to test our implementation. Once we have a FootballTeam instance, we call the iter() function on it to create the iterator, and we loop through it using while loop:

def main():

members = [f'player{str(x)}' for x in range(1, 23)] members = members + ['coach1', 'coach2', 'coach3'] team = FootballTeam(members)

team_it = iter(team)

while True: print(next(team_it))

As a recap, here is the full code for our example (the iterator.py file):

We define the class for the iterator:

class FootballTeamIterator:

def __init__(self, members):

self.members = members # the list of players and the staff

self.index = 0

def __iter__(self):

return self

def __next__(self):

if self.index < len(self.members):

val = self.members[self.index]

self.index += 1

return val

else:

raise StopIteration()

  • We define the container class:

class FootballTeam:

def __init__(self, members):

self.members = members

def __iter__(self):

return FootballTeamIterator(self.members)

We define our main function followed by the snippet to call it:

def main():

members = [f'player{str(x)}' for x in range(1, 23)] members = members + ['coach1', 'coach2', 'coach3'] team = FootballTeam(members)

team_it = iter(team)

while True: print(next(team_it)) if __name__ == '__main__': main()

Here is the output we get when executing python iterator.py:

We got the expected output, and we see the exception when we reach the end of the iteration.

Template pattern

A key ingredient in writing good code is avoiding redundancy. In object-oriented programming (OOP), methods and functions are important tools that we can use to avoid writing redundant code.

Remember the sorted() example we saw when discussing the Strategy pattern. The sorted() function is generic enough that it can be used to sort more than one data structure (lists, tuples, and named tuples) using arbitrary keys. That's the definition of a good function.

Functions such as sorted() demonstrate the ideal case. In reality, we cannot always write one-hundred-percent generic code.

In the process of writing code that handles algorithms in the real world, we often end up writing redundant code. That's the problem solved by the Template design pattern. This pattern focuses on eliminating code redundancy. The idea is that we should be able to redefine certain parts of an algorithm without changing its structure.

Real-world examples

The daily routine of a worker, especially for workers of the same company, is very close to the Template design pattern. All workers follow more or less the same routine, but specific parts of the routine are very different.

In software, Python uses the Template pattern in the cmd module, which is used to build line-oriented command interpreters. Specifically, cmd.Cmd.cmdloop() implements an algorithm that reads input commands continuously and dispatches them to action methods. What is done before the loop, after the loop, and at the command parsing part are always the same. This is also called the invariant part of an algorithm. What changes are the actual action methods (the variant part) [j.mp/templatemart, page 27].

Another software example, the Python module asyncore, which is used to implement asynchronous socket service client/servers, also uses Template. Methods such as asyncore.dispather.handle_connect_event() and

asyncore.dispather.handle_write_event() contain only generic code. To execute the socket-specific code, they execute the handle_connect() method. Note that what is executed is handle_connect() of a specific socket, not asyncore.dispatcher.handle_connect(), which actually contains only a warning. We

can see that using the inspect module:

>>> python

import inspect

import asyncore inspect.getsource(asyncore.dispatcher.handle_connect)

" def handle_connect(self):n self.log_info('unhandled connect event', 'warning')n"

Use cases

The Template design pattern focuses on eliminating code repetition. If we notice that there is repeatable code in algorithms that have structural similarities, we can keep the invariant (common) parts of the algorithms in a template method/function and move the variant (different) parts in action/hook methods/functions.

Pagination is a good use case to use Template. A pagination algorithm can be split into an abstract (invariant) part and a concrete (variant) part. The invariant part takes care of things such as the maximum number of lines/page. The variant part contains functionality to show the header and footer of a specific page that is paginated [j.mp/templatemart, page 10].

All application frameworks make use of some form of the Template pattern. When we use a framework to create a graphical application, we usually inherit from a class and implement our custom behavior. However, before this, a Template method is usually called that implements the part of the application that is always the same, which is drawing the screen, handling the event loop, resizing and centralizing the window, and so on [Python 3 Patterns, Recipes and Idioms, by Bruce Eckel, page 133].

Implementation

In this example, we will implement a banner generator. The idea is rather simple. We want to send some text to a function, and the function should generate a banner containing the text. Banners have some sort of style, for example, dots or dashes surrounding the text. The banner generator has a default style, but we should be able to provide our own style.

The generate_banner() function is our Template function. It accepts, as an input, the text (msg) that we want our banner to contain, and the style (style) that we want to use. The generate_banner() function wraps the styled text with a simple header and footer. In reality, the header and footer can be much more complex, but nothing forbids us from calling functions that can do the header and footer generations instead of just printing simple strings:

def generate_banner(msg, style):

print('-- start of banner --') print(style(msg))

print('-- end of banner --nn')

The dots_style() function simply capitalizes msg and prints 10 dots before and after it:

def dots_style(msg):

msg = msg.capitalize()

msg = '.' * 10 + msg + '.' * 10 return msg

Another style that is supported by the generator is admire_style(). This style shows the text in uppercase and puts an exclamation mark between each character of the text:

def admire_style(msg):

msg = msg.upper()

return '!'.join(msg)

The next style is by far my favorite. The cow_style() style executes the milk_random_cow() method of cowpy, which is used to generate a random ASCII art character every time cow_style() is executed. If cowpy is not already installed on your system, you can install it using the pip install cowpy command.

Here is the cow_style() function:

def cow_style(msg):

msg = cow.milk_random_cow(msg) return msg

The main() function sends the text happy coding to the banner and prints it to the standard output using all the available styles:

def main():

msg = 'happy coding'

[generate_banner(msg, style) for style in (dots_style, admire_style, cow_style)]

The following is the full code of the example (the template.py file):

We import the cow function from cowpy:

from cowpy import cow

  • We define the generate_banner() function:

def generate_banner(msg, style):

print('-- start of banner --') print(style(msg))

print('-- end of banner --nn')

We define the dots_style() function:

def dots_style(msg):

msg = msg.capitalize()

msg = '.' * 10 + msg + '.' * 10 return msg

Next, we define the admire_style() function:

def admire_style(msg):

msg = msg.upper()

return '!'.join(msg)

Next, we define our last style function, cow_style():

def cow_style(msg):

msg = cow.milk_random_cow(msg)

return msg

  • We finish with the main() function and the snippet to call it:

def main():

styles = (dots_style, admire_style, cow_style)

msg = 'happy coding'

[generate_banner(msg, style) for style in styles] if __name__ == "__main__":

main()

Let's take a look at a sample output by executing python template.py. (Your cow_style() output might be different due to the randomness of cowpy.):

Do you like the art generated by cowpy? I certainly do. As an exercise, you can create your own style and add it to the banner generator.

Another good exercise is to try implementing your own Template example. Find some existing redundant code that you wrote and see if the Template pattern is applicable.

Summary

In this chapter, we covered the interpreter, strategy, memento, iterator, and template design patterns.

The interpreter pattern is used to offer a programming-like framework to advanced users and domain experts, but without exposing the complexities of a programming language. This is achieved by implementing a DSL.

A DSL is a computer language that has limited expressiveness and targets a specific domain. There are two categories of DSLs: internal DSLs and external DSLs. While internal DSLs are built on top of a host programming language and rely on it, external DSLs are implemented from scratch and do not depend on an existing programming language. The interpreter is related only to internal DSLs.

Musical notation is an example of a non-software DSL. The musician acts as the Interpreter that uses the notation to produce music. From a software perspective, many Python template engines make use of Internal DSLs. PyT is a high-performance Python DSL to generate (X)HTML.

Although parsing is generally not addressed by the Interpreter pattern, in the implementation section, we used Pyparsing to create a DSL that controls a smart house and saw that using a good parsing tool makes interpreting the results using pattern matching simple.

Then, we saw the Strategy design pattern. The strategy pattern is generally used when we want to be able to use multiple solutions for the same problem transparently. There is no perfect algorithm for all input data and all cases, and by using Strategy, we can dynamically decide which algorithm to use in each case.

Sorting, encryption, compression, logging, and other domains that deal with resources use Strategy to provide different ways to filter data. Portability is another domain where Strategy is applicable. Simulations are yet another good candidate.

We saw how Python, with its first-class functions, simplifies the implementation of Strategy by implementing two different algorithms that check if all the characters in a word are unique.

Next, we saw the Memento pattern, used to store the state of an object when needed. Memento provides an efficient solution when implementing some sort of undo capability for your users. Another usage is the implementation of a UI dialog with Ok/Cancel buttons, where if the user chooses to cancel, we would restore the initial state of the object.

We used an example to get a feel for how Memento, in a simplified form and using the pickle module from the standard library, can be used in an implementation where we want to be able to restore previous states of data objects.

The Iterator pattern gives a nice and efficient way to iterate through sequences and collections of objects. In real life, whenever you have a collection of things and you are getting to those things one by one, you are using a form of the Iterator pattern.

In Python, Iterator is a language feature. We can use it immediately on built-in containers (iterables) such as lists and dictionaries, and we can define new iterable and iterator classes, to solve our problem, by using the Python iterator protocol. We saw that with an example of implementing a football team.

Lastly, we discussed the Template pattern. We use Template to eliminate redundant code when implementing algorithms with structural similarities.

We saw how the daily routine of a worker resembles the Template pattern. We also mentioned two examples of how Python uses Template in its libraries. General use cases of when to use Template were also mentioned.

We concluded by implementing a banner generator, which uses a Template function to implement custom text styles.

In the next chapter, we will cover the Observer pattern in reactive programming.

[ 178 ])

  • 4

'Mastering Python Design Patterns' 카테고리의 다른 글

The Observer Pattern in Reactive Programming  (0) 2023.03.22
The State Pattern  (0) 2023.03.22
The Observer Pattern  (0) 2023.03.22
The Command Pattern  (0) 2023.03.22
The Chain of Responsibility  (0) 2023.03.22

댓글