Feineigle.com - Practical Python Design Patterns

Home · Book Reports · 2018 · Practical Python Design Patterns

Published: July 21, 2018 (5 years 8 months ago.)
Tags:  Programming · Python



The book in...
One sentence:
A rehash/update of the Gang of Four done by forcing python into a Java-like box, no thanks.

Five sentences:
The patterns are, overall worth understanding; the more tools you have in your toolbox, the more exposure you have to have various problems can be decomposed, the better. That said, some of the coding style I think could be more harmful than useful. In general I think there is too much emphasis on a Java-like use of abstract base classes (even when you can finally omit the abc), something that doesn't feel like it is in the spirit of python, but, they are a tool, and have their place. My second issue is that there are simply too many classes with 2 methods, 1 being __init__; It feels like the author has overdosed on the object oriented Kool-Aid, sees every problem, ironically after talking about the right tool for the right job, as a problem that should be solved with OOP. The best part of the book, in my opinion, comes in chapter one where there is some solid advice on learning, continued learning, deliberate learning.

designates my notes. / designates important.


Thoughts

Overall

I don’t necessarily disagree with what is being said, the patterns themselves are perfectly reasonable, and maybe his style works better on very complex programs, that I don’t write, but it seems like his is adding unnecessary classes and complexity in the name of extensive “decoupling”.

A python talk I liked titled: Stop Writing Classes, is seemingly antithetical to this book’s advice, which is rife with classes. Particularly, if you have a class with two methods, one being init, you probably don’t have a class. Many of the classes in this book fit the bill.

After re-watching Stop Writing Classes, that I agree very much with, presented by a python core developer, I think this author seems to be trying to force python into Java-like object oriented box. The book does, in fact, mention Java-like interfaces several times.

Personally I prefer more functional programming to OOP, but that doesn’t make one better or worse. And, while I wasn’t a huge fan of this book, it was interesting, a different perspective than my normal one, and, if nothing else, it was a breeze to read.

My biggest pet peeve comes toward the end of the book when the author talks about constantly upgrading you code. Adding new features. Maybe I am getting to be a curmudgeon, but I prefer FEWER updates (security aside).

You should build your programs in such a way that you can constantly swap out bits of the program without changing any other part of the code. Every period—depending on the size and scope of your project, this might mean a year or five years or six months -you want to have cycled out all the old code and replaced it with better, more elegant, and more efficient code.

Let me repeat that last line:

…five years or six months -you want to have cycled out all the old code and replaced it with better, more elegant, and more efficient code.

While I don’t agree with much of the book, this part here is the worst. Constantly updating your code is the bane of software, not the holy grail. It will lead to bloat and bugs. Code should be updated as little as possible. Write it right, IMHO.

How often is grep updated? sed? awk? And they are great tools, timeless. How about Firefox? Every few weeks? And it is a bloated POS. Good code should move towards being static, despite what the buzz is about dynamic, constantly changing. As far as I am concerned, constantly updated code exists primarily to keep developers fed. Imagine any other industry providing “updates”. Oh, I have to bring my car in AGAIN? I need a patch on my refrigerator or my food will spoil!? It wouldn’t fly. Yet in the tech world, shoddy, not-yet-ready code is lauded as dynamic and agile.

Chapter-by-Chapter

Chapter one starts typically, with the obligatory setting up your environment. This seems ridiculous to me, given the level you’d be at if you are learning patterns.

It offers comparisons and some critique of the classic Gang of Four book, which introduced, or at least mainstreamed, the concept of patterns into the programming world.

There is some great advice on learning: go slowly and deliberately, talk to others who are more skilled than you, and code, code, code. Write something, throw it out and do it again if you have to. This will also help you learn that it is sometimes OK to chuck out ten thousand lines if you now see how you can solve the problem in one hundred lines. Don’t think of the ten thousand as wasted, you needed to write them to get to the one hundred.

From here, each chapter goes on to focus on a particular pattern. It is usually introduced with suboptimal code and then, through a series of revisions, if ends up taking the final shape of the pattern in question.

The first pattern covered is the singleton. While there are many uses that this is perfectly fine using, like logging or load balancing, it is really a fancy way to have a global variable. As with all of the patterns and advice in the book, none of them are catch-all; each is a unique tool that should be deployed only when appropriate.

Next is the prototype pattern. The example give is of a real-time strategy game where you need to construct units. It seems to create new units by copying old units, but it is unclear how the first unit is created. It does clearly separate the values, statistics, and abilities, of units into data read from disk, and is not included in the code itself. This seems obvious to me, but what do I know?

The factory is the first one I am not really sold on. I’m missing the benefit over straight inheritance. A @staticmethod that takes a string defining what to construct, or a number of methods, one for each thing you want to construct. You’ll need an if statement in the former and a number of methods in the latter. As I’ll repeat again and again, I could be missing something with this.

With the builder, like the factory, I don’t see the benefit. You still need a method for each part of the, in the example’s case, form. Using several classes, creating objects that reference one another, seems messy. Maybe in a larger code base than the example or I am used to working with (only my little personal programs), but I’m not seeing it jump out at me why this is better than a function to decide what other function will be called, all in one class.

def create(self, field_list):
  for field in field_list:
    if field["type"] == "text_field":
      buildTextField(field)
    elif field["type"] == "whatever_field":
      buildWhateverField(field):
    else:
      buildBlahField(field)

def buildTextField(field):
  # build text field
  return text_field

Again, I am probably missing something, but this is what I would do. I would need to add an elif and a method for building each new field type, but you’d have to do the same with the builder pattern.

The adapter pattern I completely agree with. If you have a library that provides something, but not exactly the way you’d like it, you can create an adapter to handle what you require. You can even pass everything else that you don’t need to modify off to the base library to handle.

Here the open/closed principle is also discussed. Code should be open to extension, but closed to modification. You can use adapters (is this the same as wrapper?) to extend code without have access to, or touching, the source.

I never thought of decorators as a pattern, but it isn’t unreasonable. The pattern of having to do something before or after a number of functions. There is really not much to say about these, but class decorators were something I wasn’t aware of. It mentions Flask as a good source to read to see decorators used interestingly (the routing).

The facade pattern feels much like an interface, a single point, likely a class, that can be seen externally. All the messy business is hidden behind a clean implementation. The guts can be modified or swapped out and the facade should keep on working on the client side without modification.

This one seems reasonable, kind of like an adapter on steroids. I can see it being more valuable if you are building a library or something many other people, who want a simple, clean product.

With the proxy pattern, you want the interface to remain constant, with some actions taking place in the background. Conversely, the adapter pattern is targeted at changing the interface.

the example, hiding the memoization of a Fibonacci generating function, is another example showing how to hide the guts of a program behind simpler interface.

The chain of responsibility is on of the more confusing patterns, but upon sussing it out, I like it. It uses handlers to handle certain kinds or parts of a request.

You can use a dispatcher to create a chain of handlers (functions) and then iterate through them, each iteration will produce an updated request and pass it to the next handler. a catch all handler at the end cleans up the leftovers.

The command pattern tries to decouple commands from execution, creating essentially an execution queue, that gives you an undo stack for free. It feels like a producer-consumer. What I wonder is, why not use an actual queue?

The interpreter pattern hinges on domain specific language. This is one of my least favorite patterns, but is a neat solution if you have some very specific, domain, problem to solve.

The example was a restaurant that had complex rules for discounts. You can use the interpreter to allow the restaurateur to write, in a domain specific language, how the specials will be provided. This DSL is then interpreted by the python program.

Here is another place where I see the author as being class happy. Everything, even the day of week and time become (unnecessarily IMHO) objects.

In contrast, one of my favorite patterns is that of the iterator. At the end of this chapter it says something like: this will lead you into the realm of functional programming. Makes sense that I like this pattern a lot.

The observer pattern is another one I am all on board with. The author seems to be letting up on the classes, showing that the abstract base class isn’t really needed with python’s duck typing. He even says it forces a Java-like interface. Why would I want to force python to be Java-like?

I watched a few videos, to dig deeper, on ABC and python, and my takeaway was, as was one of the presenters, that they aren’t really in the spirit of python, but they are there if you need that specific tool.

The decoupling and loose coupling concepts I am on board with, though I’ve heard a lot of people say that language simply doesn’t come up as much as you’d think, given how drummed into modern CS students they are.

With the state machine, which in general I love, the author goes class crazy again, making every state its own class. Dictionaries would probably work, although it would be problem dependent.

The strategy pattern tries to avoid ’long’ if-else blocks, something the author seems to despise. I wonder what he thinks about case statements?

You can use an executor function initiated with another function that implements the particular strategy you want, at run-time.

I also like this pattern. Though the chapter started out by writing several (unnecessary) classes, it made its way back to only 1 executor function and x strategy functions.

The template method; another pattern I like, when appropriate. The author argues for the use of an abstract base class and I might agree this time. if the classes you are creating have many functions that must be implemented the extra sanity check provided by the abc will be useful. That is to say, if you need to implement a new class, SystemX, down the road, will you remember that it must implement step1 … stepX without forgetting any? the abc forces you to implement all of the require methods.

The visitor pattern might be ok, but the way he implements it: no fucking way. 19 classes to control 5 peripherals with settings for two people. most classes have only init and one other method. No, no, no.

I would use classes to control the peripherals, but I would have 1 class per peripheral (5 in this example), 1 class for the person, 1 class (maybe) for a controller. Person has all the settings that are then broadcast to the peripherals, the controller could mediate between multiple present persons.

State machine where the states are the people that are home. I.e. P1+P2, P1+!P2, !P1+P2, !P1+!P2, would possibly simplify it farther.

Not much to say about model-view-controller, it feels like common sense to me. Anyone who has used Flask or Django will be familiar with this. Really anyone who has a smidgen of coding sense will recognize that separating responsibility of code is more than a reasonable goal.

Last we have the publisher-subscriber, which is really a rehash of the observer. The irony is how the author goes out of his way, through the whole book to decouple everything, but, and this is a perfect example, you can’t totally decouple everything. I think I would go about this a little different, probably approach it from an API standpoint, have the point of contact be the published server where the client subscribers occasionally ping for updates. RSS really.

All in all, I would probably not recommend this book to someone wanting to learn about these patterns. I’d probably call the book intermediate to advanced, but I think the author is forcing python to be Java-like, and I don’t condone that.


Exceptional Excerpts

“Work on something that you cannot yet do."


Table of Contents


· 01: Before We Begin

page 5:

page 9:
page 10:

· 02: The Singleton Pattern

· 03: The Prototype Pattern

page 56:

· 04: Factory Pattern

page 67:
page 68:

· 05: Builder Pattern

· 06: Three Illustrations

page 99:
from third_party import WhatIHave

class ObjectAdapter(object):
  def __init__(self, what_i_have):
    self.what_i_have = what_i_have

  def required_function(self):
    return self.what_i_have.provided_function_1()

  def __getattr__(self, attr):
    # Everything else is handeled by the wrapped object
    return getattr(self.what_i_have, attr)
page 100:
page 102:
class ObjectAdapter(object):
  def __init__(self, what_i_have, provided_function):
    self.what_i_have = what_i_have
    self.required_function = provided_function

  def __getattr__(self, attr):
    return getattr(self.what_i_have, attr)

· 07: Decorator Pattern

page 116:
def dummy_decorator(f):
  def wrap_f():
    print("Function to be decorated: ", f.__name__)
    print("Nested wrapping function: ", wrap_f.__name__)
    return f()
  wrap_f.__name__ = f.__name__
  wrap_f.__doc__ = wrap_f.__doc__ #i think this should be f.__doc__
  return wrap_f

@dummy_decorator
def do_nothing():
  print("Inside do_nothing")

if __name__ == "__main__":
  print("Wrapped function: ",do_nothing.__name__)
  do_nothing()
from functools import wraps

def dummy_decorator(f):
  @wraps(f)
  def wrap_f():
    print("Function to be decorated: ", f.__name__)
    print("Nested wrapping function: ", wrap_f.__name__)
    return f()
  return wrap_f

@dummy_decorator
  def do_nothing():
  print("Inside do_nothing")

if __name__ == "__main__":
  print("Wrapped function: ",do_nothing.__name__)
  do_nothing()
page 117:
import time
from functools import wraps

def profiling_decorator_with_unit(unit):
  def profiling_decorator(f):
    @wraps(f)
    def wrap_f(n):
      start_time = time.time()
      result = f(n)
      end_time = time.time()
      if unit == "seconds":
        elapsed_time = (end_time - start_time) / 1000
      else:
        elapsed_time = (end_time - start_time)
      print("[Time elapsed for n = {}] {}".format(n, elapsed_time))
      return result
    return wrap_f
  return profiling_decorator

@profiling_decorator_with_unit("seconds")
def fib(n):
  print("Inside fib")
  if n < 2:
    return
  fibPrev = 1
  fib = 1
  for num in range(2, n):
    fibPrev, fib = fib, fib + fibPrev
  return fib

if __name__ == "__main__":
  n = 77
  print("Fibonacci number for n = {}: {}".format(n, fib(n)))

· 08: Facade Pattern

page 132:

· 09: Proxy Pattern

page 138:
import time

class RawCalculator(object):
  def fib(self, n):
    if n < 2:
      return 1
    return self.fib(n - 2) + self.fib(n - 1)

def memoize(fn):
  __cache = {}
  def memoized(*args):
    key = (fn.__name__, args)
    if key in __cache:
      return __cache[key]
    __cache[key] = fn(*args)
    return __cache[key]
  return memoized

class CalculatorProxy(object):
  def __init__(self, target):
    self.target = target
    fib = getattr(self.target, 'fib')
    setattr(self.target, 'fib', memoize(fib))

  def __getattr__(self, name):
    return getattr(self.target, name)

if __name__ == "__main__":
  calculator = CalculatorProxy(RawCalculator())
  start_time = time.time()
  fib_sequence = [calculator.fib(x) for x in range(0, 80)]
  end_time = time.time()
  print("Calculating the list of {} Fibonacci numbers took {}seconds".format(len(fib_sequence),
                                                                             end_time - start_time))
page 140:

· 10: Chain of Responsibility Pattern

· 11: Command Pattern

· 12: Interpreter Pattern

page 201:

· 13: Iterator Pattern

page 213:

· 14: Observer Pattern

page 230:
class ConcreteObserver(object):
  def update(self, observed):
    print("Observing: " + observed)

class Observable(object):
  def __init__(self):
    self.observers = set()

  def register(self, observer):
    self.observers.add(observer)

  def unregister(self, observer):
    self.observers.discard(observer)

  def unregister_all(self):
    self.observers = set()

  def update_all(self):
    for observer in self.observers:
      observer.update(self)
page 232:

· 15: State Pattern

· 16: Strategy Pattern

page 253:
def executor(arg1, arg2, func=None):
  if func is None:
    return "Strategy not implemented..."
  return func(arg1, arg2)

def strategy_addition(arg1, arg2):
  return arg1 + arg2

def strategy_subtraction(arg1, arg2):
  return arg1 - arg2

def main():
  print(executor(4, 6))
  print(executor(4, 6, strategy_addition))
  print(executor(4, 6, strategy_subtraction))

if __name__ == "__main__":
  main()

· 17: Template Method Pattern

page 266:
import abc

class ThirdPartyInteractionTemplate(metaclass=abc.ABCMeta):
  def sync_stock_items(self):
    self._sync_stock_items_step_1()
    self._sync_stock_items_step_2()
    self._sync_stock_items_step_3()
    self._sync_stock_items_step_4()

def send_transaction(self, transaction):
  self._send_transaction(transaction)

@abc.abstractmethod
def _sync_stock_items_step_1(self): pass

@abc.abstractmethod
def _sync_stock_items_step_2(self): pass

@abc.abstractmethod
def _sync_stock_items_step_3(self): pass

@abc.abstractmethod
def _sync_stock_items_step_4(self): pass

@abc.abstractmethod
def _send_transaction(self, transaction): pass

class System1(ThirdPartyInteractionTemplate):
  def _sync_stock_items_step_1(self):
    print("running stock sync between local and remote system1")

  def _sync_stock_items_step_2(self):  
    print("retrieving remote stock items from system1")

  def _sync_stock_items_step_3(self):
    print("updating local items")

  def _sync_stock_items_step_4(self):
    print("sending updates to third party system1")

  def _send_transaction(self, transaction):
    print("send transaction to system1: {0!r}".format(transaction))

class System2(ThirdPartyInteractionTemplate):
  def _sync_stock_items_step_1(self):
    print("running stock sync between local and remote system2")

  def _sync_stock_items_step_2(self):  
    print("retrieving remote stock items from system2")

  def _sync_stock_items_step_3(self):
    print("updating local items")

  def _sync_stock_items_step_4(self):
    print("sending updates to third party system2")

  def _send_transaction(self, transaction):
    print("send transaction to system2: {0!r}".format(transaction))

class System3(ThirdPartyInteractionTemplate):
  def _sync_stock_items_step_1(self):
    print("running stock sync between local and remote system3")

  def _sync_stock_items_step_2(self):  
    print("retrieving remote stock items from system3")

  def _sync_stock_items_step_3(self):
    print("updating local items")

  def _sync_stock_items_step_4(self):
    print("sending updates to third party system3")

  def _send_transaction(self, transaction):
    print("send transaction to system3: {0!r}".format(transaction))

def main():
  transaction = {"id": 1,
                 "items": [{"item_id": 1,
                            "amount_purchased": 3,
                            "value": 238
                           }],
  }

for system in [System1, System2, System3]:
  print("="*10)
  system.sync_stock_items()
  system.send_transaction(transaction)

if __name__ == "__main__":
  main()

· 18: Visitor Pattern

· 19: Model-View-Controller Pattern

page 313:

· 20: Publish-Subscribe Pattern