Delegate and Decorate in Python: Part 1 – The Delegation Pattern

Note: This is not about Python’s language feature called decorators (with the @ symbol), but about the design patterns called “decorator” and “delegate.”

In this series we’re going to learn two very useful design patterns: the delegation pattern and the decorator pattern. Each pattern is useful in that they help us enforce the single responsibility principle of object oriented programming. Getting these patterns to work as expected in Python requires a relatively deep dive into Python syntax as well as the always scary technique of metaprogramming. Let’s jump in!

Motivation

I had the idea for this article while working on LuluTest, my open source browser automation framework. One of my classes had several methods that shared identical lines either before or after the method fired, but the methods themselves did not have identical signatures or responsibilities, so there was no opportunity to abstract away the reused code using traditional inheritance or composition.

There is actually a pretty easy way to do this in Ruby, and I thought the same would be true for Python. This turned out not the case though and it took some work to figure out how to copy Ruby’s delegate feature in Python. Check out this file to see how I implemented this workaround. To save readers the trouble of figuring it out on their own, and for the added bonus of learning more about OOP, I decided to write this article.

What is Delegation?

According to Wikipedia, “the delegation pattern is an object-oriented design pattern that allows object composition to achieve the same code reuse as inheritance.” If you read that sentence four or five times you might get it but let me try to make it simpler.

Let’s say we have a Dog class that is a subclass (and thus inherits the functionality of) an Animal class. If Animal has a method called get_number_of_legs, any instantiation of the Dog class can call the get_number_of_legs method. In Python, an implementation might look like this:

class Animal:
  def __init__(self, name, num_of_legs):
    self.name = name
    self.num_of_legs = num_of_legs
  
  def get_number_of_legs(self):
    print(f"I have {self.num_of_legs} legs")

class Dog(Animal):
  def __init__(self, name, num_of_legs):
    super().__init__(name, num_of_legs)

dog = Dog('Fido', 4)
dog.get_number_of_legs()

# Outputs "I have 4 legs"

It would be technically incorrect to say that Dog delegates get_number_of_legs to Animal because the Dog class actually has that method since it inherits the Animal class. This is what the Wikipedia definition is talking about when it refers to “code reuse.” This is what delegation will duplicate when we use composition.

Kitchens are Compositions

Let’s do a pseudocode example to understand delegation before we dig into a Python implementation. For this example, let’s think of a class called Kitchen. In real life, kitchens–as rooms–do not have any functionality, but we think of a kitchen’s functionality as a composition of the appliances it has. If my kitchen has a microwave, I can heat things up in my kitchen; likewise, with a dishwasher, I can wash dishes in my kitchen.

Now let’s think about this from an object oriented design position. I want to abstract away the appliance implementations and just think of heating up food and washing dishes as kitchen functions. In other words, I want to write my code like this:

# Pseudocode
kitchen = new Kitchen();
kitchen.heat_up_food();
# Food is being microwaved
kitchen.wash_dishes();
# Dishwasher starting

In order to do this, in my Kitchen class definition, I probably have Microwave and Dishwasher classes as attributes of the Kitchen class, allowing the Kitchen class access to their methods. This is essentially what composition is. Now, let’s start really implementing this in Python so we can understand the rest of that Wikipedia definition.

Pythonic Kitchen

Ok, let’s talk about how this kitchen would look in Python code. I was thinking something like this:

class Microwave:
  def __init__(self):
    pass

  def heat_up_food(self):
    print("Food is being microwaved")

class Dishwasher:
  def __init__(self):
    pass

  def wash_dishes(self):
    print("Dishwasher starting")

class Kitchen:
  def __init__(self):
    self.microwave = Microwave()
    self.dishwasher = Dishwasher()

Just like we discussed, the Microwave and Dishwasher classes are attributes of the Kitchen class. Now our kitchen has access to their methods, right? Well, let’s see it in the console:

>>> from kitchen import Kitchen
>>> kitchen = Kitchen()
>>> kitchen.heat_up_food()
Traceback (most recent call last):
  File ".\kitchen.py", line 21, in <module>
    kitchen.heat_up_food()
AttributeError: 'Kitchen' object has no attribute 'heat_up_food'

Why doesn’t that work? Well, if we want to use the Kitchen‘s Microwave, we have to refer to it as kitchen.microwave.heat_up_food(). This is not the kind of code reuse the delegation definition is talking about. So how do we get kitchen.heat_up_food() to give us what we want?

Implementing the Delegation

The most obvious way would be to use inheritance. If we implement the Kitchen class as inheriting Microwave and Dishwasher, we can use kitchen.heat_up_food() with no syntactical problems.

However, this solution would be very poor design. Inheritance is typically saved for classes that have “is a” relationships. Earlier we created a Dog class that inherited from an Animal class because a Dog is a Animal. But a Kitchen is not a Microwave or Dishwasher, so inheritance here would not make sense.

This is why we have to delegate. One simple way of delegation would be to just create wrapper methods in the Kitchen class like so:

# ... Microwave and Dishwasher truncated
class Kitchen:
  def __init__(self):
    self.microwave = Microwave()
    self.dishwasher = Dishwasher()

  def  heat_up_food(self):
    self.microwave.heat_up_food()

  def wash_dishes(self):
    self.dishwasher.wash_dishes()

Now, the same thing we did in the Python console earlier works as we want:

>>> from kitchen import Kitchen
>>> kitchen = Kitchen()
>>> kitchen.heat_up_food()
Food is being microwaved
>>> kitchen.wash_dishes()
Dishwasher starting

Now, when we call heat_up_food on the Kitchen class, we are actually delegating it to the Microwave class. We are getting closer to that “code reuse” for this composite class, but we’re not quite there yet.

The reason we’re “not there” yet is because we haven’t really reused the Microwave and Dishwasher methods, we just wrapped them in identically named functions in the Kitchen class. And really, this is fine if your composite class is only going to have one or two methods to delegate. But what if we want to extend the Microwave and Dishwasher classes with more methods? Now, every time we write a method in those classes, we have to go write another wrapper in the Kitchen class to delegate those functions. This is not code reuse, it’s duplication.

So what are our options? Well, I’m afraid we might have to start using metaprogramming to get what we want.

__getattr__, getattr, and dir

We are going to use three builtin methods that don’t get a lot of use outside of general Python wizarding. Let’s talk about __getattr__, getattr, and dir.

__getattr__ is a function that all Python classes have. This method is called by default anytime an unknown or non-existent attribute is called on a class. For example, earlier when we called kitchen.heat_up_food() before we delegated the method, it was the builtin __getattr__ method which raised that AttributeError.

getattr is a builtin Python function that takes as input a class instance and attribute, and returns the designated class’s designated attribute. We can even execute class functions by appending a () at the end. Let’s go back to the Dog class we created earlier for an example:

>>> from animal import Dog
>>> dog = Dog('fido', 4)
>>> getattr(dog, 'name')
'fido'
>>> getattr(dog, 'get_number_of_legs')()
I have 4 legs

Finally, dir is a function that returns an array of strings for all the method names in a class instantiation. Check it out on Dishwasher:

>>> dishwasher = Dishwasher()
>>> dir(dishwasher)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'wash_dishes']

We will combine these three methods in the Kitchen class to delegate all Dishwasher and Microwave functions.

Delegating the Kitchen

So let’s think about what we know. The dir method gives us a list available methods inside of a class (the ones starting with and underscore are Python built-ins that we can ignore), and the __getattr__ method is called when a non-existent attribute is called on a class.

What we should do, then, is overwrite the Kitchen‘s __getattr__ method and check and see if the non-existent attribute exists in either its Dishwasher or Microwave attributes. If it is, we can use the getattr method to call the requested method on the appropriate class. Here’s how we’ll do it:

# Microwave and Dishwasher are the same as earlier

class Kitchen:
  def __init__(self):
    self.microwave = Microwave()
    self.dishwasher = Dishwasher()
    self.microwave_methods = [f for f in dir(Microwave) if not f.startswith('_')]
    self.dishwasher_methods = [f for f in dir(Dishwasher) if not f.startswith('_')]
  
  
  def __getattr__(self, func):
    def method(*args):
      if func in self.microwave_methods:
        return getattr(self.microwave, func)(*args)
      elif func in self.dishwasher_methods:
        return getattr(self.dishwasher, func)(*args)
      else:
        raise AttributeError
    return method

What’s going on here? Well, in the Kitchen‘s constructor, we not only set the microwave and dishwasher attributes, we also set an array of available methods from within those classes (we ignore the methods that start with an underscore because those are Ptyhon built-in methods and we don’t want to hijack those).

Then, we overwrite Kitchen‘s __getattr__ method by defining another method within it. Since this method will be called anytime an attribute that doesn’t exist in Kitchen is called, this is the ideal place to do our delegation. We catch the called method in the func argument, then check if it is within the list of available methods in Microwave or Dishwasher. If it is, we call that function with (*args), which represents any arguments the non-existent function might have been called with (though in our current case, this is irrelevant).

This has probably been confusing so let’s see it in action and talk about what is actually going on. In your Python console:

>>> from kitchen import *
>>> kitchen = Kitchen()
>>> kitchen.heat_up_food()
Food is being microwaved
>>> kitchen.wash_dishes()
Dishwasher is starting

Ok so here’s what happened. We created an instance of Kitchen called kitchen which is obvious. When we called heat_up_food, kitchen‘s __getattr__ method was called, because heat_up_food is not a function or attribute of Kitchen. Inside this method, we checked to see if the called method existed in microwave_methods, which it did. Because it did, we used getattr to call kitchen‘s heat_up_food on its microwave instance and returned that method. That’s why it looks like kitchen.heat_up_food() did exactly what we wanted.

Note: The line raising the AttributeError is absolutely necessary. Without it, any otherwise non-existent method/attribute calls will error silently. In other words, something like kitchen.hey_look_at_me() will simply return nothing.

Why is This Better?

Functionally, this looks the same as what we had before. However, unlike the previous implementation of our delegator pattern, we can add methods to Microwave and Dishwasher with reckless abandon and know that they will be available in the Kitchen class without any other work from us. To illustrate, let’s extend the Microwave class:

class Microwave:
  def __init__(self):
    pass

  def heat_up_food(self):
    print("Food is being microwaved")
  
  def timed_heat(self, minutes):
    print(f"Microwaving the food for {minutes} minutes")

Now, we don’t have to do anything else to our Kitchen class to allow it to use this new timed_heat class. In the console:

>>> from kitchen import *
>>> kitchen = Kitchen()
>>> kitchen.timed_heat(4)
Microwaving the food for 4 minutes

And that is the true code reuse achievement we get from inheritance without actually using inheritance. With some magic, we have pooled the functionality of a kitchen’s surrogate parts to abstract those parts away.

One note about this particular implementation is that it assumes the classes composing Kitchen do not have any attributes. If the Microwave class had an attribute called color for example, calling Kitchen.color would return an error. We will talk more about this when we get into Decorators in the next installment of this series.

Delegation Wrap-Up

This technique is not without its downsides. For example, since delegated methods aren’t explicitly defined inside composite classes, your IDE might have a hard time offering code completion for delegated methods. In fact, it might even highlight calls to delegated methods as potential errors since, when analyzed statically, it seems like those methods don’t exist.

Additionally, the use of metaprogramming (which is what we did when we overwrote the __getattr__ method) can slow scripts down. What you lose in execution speed, you might gain in development velocity however; all software design decisions are trade-offs.

This pattern is a powerful tool in object oriented programming and for making code readable and extendable. It is also paramount to the next pattern we cover in this series: the Decorator Pattern.