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.
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
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
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!!")
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!!"
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.
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.
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.
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
Another method we're going to use is
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?
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
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!!")
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)
And when I run this in my console I get
RUFF! Let's go!! Jeff
So we know it works!
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
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.
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
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
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.
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.
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!