DEV Community

Cover image for Implementing the Decorator Pattern in Python
Erik
Erik

Posted on

Implementing the Decorator Pattern in Python

Good evening everyone, today we're going to learn one of my favorite design patterns, the decorator pattern. Please note that this is not about the Python language construct of the same name. This is about a design pattern, the concepts of which extend to most languages.

What is the Decorator Pattern?

Put simply, the decorator pattern is a means to add functionality to an object at runtime. If you're into UML, here's Wikipedia's class diagram of the pattern:
Decorator Pattern UML

When to Use the Decorator Pattern

Decorators are typically used in cases where it might seem like you want to reach for inheritance, but from a design perspective, it wouldn't make sense.

For example, you may have a class called Dog and you need it to have a method called eat, you could probably put eat in an Animal super-class that Dog and other animals inherit, thus gaining the eat functionality.

But what if you have a Dog that's on a leash? It might behave differently than a regular dog. We could make a class called LeashedDog that inherited Dog. That would give us a Dog object with all the Dog functionality as well as the LeashedDog functionality.

However, let's think about what that implementation might look like. The classes would be like this:

# Leashed dog with inheritance
class Dog:
  def __init__(self, name):
    self.name = name

  def bark(self):
    print("RUFF!")

class  LeashedDog(Dog):
  def __init__(name):
    super().__init__(name)

  def tug_on_leash(self):
    print("Let's go!!")
Enter fullscreen mode Exit fullscreen mode

And using these classes:

dog = Dog('Jeff')
dog.bark()
# outputs "RUFF!"
dog = LeashedDog(dog.name)
dog.bark()
# outputs "RUFF!"
dog.tug_on_leash()
# outputs "Let's go!!"
Enter fullscreen mode Exit fullscreen mode

And in this case that might be fine. But in this writer's humble opinion, it's poor design and I'll tell you why.

How I decide when to use a decorator

In the above example, we created a new instance of a dog. Our dog was still named Jeff, it could still bark, but creating a new instance kind of implies that it's a new dog.

The times when you should use decoration vs. inheritance is not always cut and dry. In fact, some might believe the above design is just fine, and really, it is. However, you may need this pattern for something more complex one day, so I'm keeping it simple so you'll understand it better.

If there was a rule-of-thumb for when to use decoration over inheritance, it would be when you have composite classes--classes that have other classes instead of classes that are other classes.

I usually opt for decoration when a new instance (like a new dog) doesn't seem quite right. Instead, I'm adding new abilities to an existing thing. Jeff can't tug on a leash until I put one on him.

Time to get meta

We're about to lightly dip our toes into the ocean of metaprogramming; changing the default behavior of the Python interpreter. There are very, very few times that metaprogramming is the answer to your problems, so do not abuse this technique.

With that warning out of the way, as far as metaprogramming goes, we're actually not going to do anything too daring.

Introducing __getattr__ and getattr

In Python, classes will have what are called "built in" functions normally starting with double underscores (__). These functions have designated functionality and are used in ways normally hidden from us.

One such method is the __getattr__ method that all classes have. This method is called anytime you try to call a non-existent attribute or method on an object. For example, if we said dog.chase_tail(), the Python interpreter tries to find chase_tail in the dog object, can't, and then calls __getattr__, raising an AttributeError.

Another method we're going to use is getattr. getattr takes as input parameters an object and stringified name of a function, then returns the function on the object. In our previous example, if we write getattr(dog, 'bark'), we'll get the function object back. Even better, we can do getattr(dog, 'bark')() and get the console to print "RUFF!". Isn't that cool?

The decorator in action

How do we want our decorator to act? Well, we want all the methods of Dog, with an added one, tug_on_leash. We don't want to use inheritance though, so somehow we have to preserve the functionality the Dog class.

Here's the plan. We're going to make a class called LeashedDogDecorator (you should always name design-pattern-component classes with their name like 'decorator', 'observer', etc.). We're going to expect a model in the initialization--which in our case will be a dog object--, and we're going to use __getattr__ to delegate calls to Dog methods to the model instance variable. That was a lot of words so let's just show it:

# Leashed dog with decoration
class Dog:
  def __init__(self, name):
    self.name = name

  def bark(self):
    print("RUFF!")

class LeashedDogDecorator:
  def __init__(self, model):
    self.model = model
    self.model_attributes = [attribute for attribute in self.model.__dict__.keys()]
    self.model_methods = [m for m in dir(self.model) if not m.startswith('_') and m not in self.model_attributes]

  def __getattr__(self, func):
    if func in self.model_methods:
      def method(*args):
        return getattr(self.model, func)(*args)
      return method
    elif func in self.model_attributes:
      return getattr(self.model, func)
    else:
      raise AttributeError

  def tug_on_leash(self):
    print("Let's go!!")
Enter fullscreen mode Exit fullscreen mode

YIKES!!

Before we talk about what's going on, let's make sure everything is working. At the bottom of my script I have:

dog = Dog('Jeff')
dog = LeashedDogDecorator(dog)
dog.bark()
dog.tug_on_leash()
print(dog.name)
Enter fullscreen mode Exit fullscreen mode

And when I run this in my console I get

RUFF!
Let's go!!
Jeff
Enter fullscreen mode Exit fullscreen mode

So we know it works!

What the heck is going on?

Let's go line by line starting with the constructor. We pass model, which we know is going to be an instance of a Dog class, which we then assign to the decorator class's model attribute.

Then, we gather the list of attributes associated with the model (which, again, is a Dog instance) by using the __dict__ built in, which returns a dictionary of a class's attributes and values (we are only interested in the attribute names, hence the keys() call). Then, we gather all the methods in the class that are not in the model_attributes list, and do not start with an underscore (because those are usually Python built ins).

Then, we overwrite the built in __getattr__ method for this class. Remember, any time a non-existent method is called on a class, __getattr__ is called. That's why func is in the method signature, it collects the name of the attribute that was tried.

Now, since LeashedDogDecorator is technically its own class, methods like bark() and attributes like name won't exist on it. That's why we've overwritten the __getattr__ method, to catch attempts to call Dog methods.

When the attempted call is in the model_methods list, we have to build a new method, call it with whatever arguments were passed along with it (hence the *args), and return it. We do this by using getattr and feeding it the model and name of the called function.

Conversely, when the attempted call is in the model_attributes list, we know we're looking for an attribute and thus don't want to call it, so we simply return that value. We do this again by using getattr(model, func).

The last case in the if..elif..else block is to raise an AttributeError any time the called attribute doesn't exist on either the Dog instance or the LeashedDogDecorator. If we don't do this, any calls to non-existent attributes on this class will simply fail silently, which we don't want.

Finally, we add the tug_on_leash method to the decorator, which is the actual decorated functionality. Now the class we're decorating has the new functionality.

Step by Step in the Interpreter

Let's try to fully understand what happens when we call bark() on our decorated method. The bark method is called on the LeashedDogDecorator class, but it doesn't exist. By default, the __getattr__ method is called. When it is, we feed the name bark and the model of our dog to getattr to run the method, giving us the appearance of a Dog that just happens to now have leash-specific-behavior.

Technicalities, Warnings, and Conclusion

So this was definitely a doozy. You might be wondering why we'd go through all the trouble of writing that decorator class when the inheritance strategy worked just fine, and that's definitely a valid question.

Sometimes, inheritance is better, and in our example, it is probably the right way to go. But in complex systems, simple implementations aren't always viable, or they come at the cost of decreased "velocity" (a project-manager word for developer productivity).

This implementation, given our example, is certainly overkill, but there are ways to abstract away some of the tedious details and write simpler looking decorators. I recently wrote a three part series on Python decorators, with better examples and much much more detail on my personal blog. If you're interested and don't mind my shameless plug, part one can be found here.

A few warnings about using metaprogramming before we go. It is always dangerous to alter the default behavior of the language you're working with. Once you do this, you're essentially voiding the warranty and are on your own. Remember in our example we added the final else clause to raise an AttributeError? I only know to do that because when I implemented this in a project of my own and couldn't figure out why I was having errors that were going unreported. You have to be careful.

Another downside to the meta approach is that it slows down execution of your scripts. Sometimes this is acceptable; but all things come at a cost.

Anyway, if you have any questions, comments, or know a better implementation of this pattern (or just want to make fun of my code), feel free to leave a comment or @ me on Twitter. Happy decorating!

Top comments (0)