Imagine you’re the new head chef at Pythonia - and you’re adding your favorite recipes to the menu so that the other cooks can prepare your creations to the hungry guests. 🍕
There are a lot of steps required to make a pizza:
Roll the dough.
Spread the tomato sauce.
Sprinkle the cheese.
Prepare and arrange the topping.
Bake it in the oven.
Serve it on a plate.
But when you’re creating the new recipes, you’re probably only changing the bit in bold for the topping. You don’t want to write out the need to roll the dough or put the cooked masterpiece on a plate in every recipe.
You don’t want to get lost in the details of other stages right now, because your job is to concentrate on the toppings.
It would help if you had a way to separate the responsibility for the part that changes (arranging the topping) from the parts that don’t (preparing the base, cooking the pizza, etc.)
It sounds simple enough! So if it were in Python code, we could just put the repeated tasks in separate functions and call them when necessary?
Yes, that would help! Then we’re not micromanaging exactly how the tomato sauce gets spread or on which plate it gets served.
But our code would look like this - with lots of repeated code as each pizza function has to list every step:
def hawaiian():# Preparation tasks for any pizzaroll_dough()spread_tomato_sauce()sprinkle_cheese()# Tasks unique to this pizzachop_pineapple()dice_ham()# Final tasks for any pizzabake_in_pizza_oven()serve_on_plate()def vegetariana():# Preparation tasks for any pizzaroll_dough()spread_tomato_sauce()sprinkle_cheese()# Tasks unique to this pizzaslice_peppers()dice_onion()chop_tomatoes()# Final tasks for any pizzabake_in_pizza_oven()serve_on_plate()
But what’s so bad about repeated code? At least it’s clear what’s going on.
True - but imagine that you had to add an extra step for all the pizzas like
remove_dough_from_plastic_wrapper(). You have to add this to every single pizza function. (And Pythonia serves a lot of different pizzas!) If you forget to do this somewhere, then a hungry customer gets served a ham and plastic pizza. Ugh! 🤢
How to Use the Decorator Pattern
As you might have guessed, there’s something called the decorator design pattern that simplifies this sort of code and helps you write maintainable code. 🎉
It works like this:
Create a decorator function that accepts another function as input and returns a decorated variation of that function as output.
Wait - you can use functions as arguments to other functions in Python?!
It’s seldom done, but yes! In Python, functions are first-class objects, which means that you can treat them just like any other variable, including using them as arguments and return values for other functions! You’ll see this in action shortly.
For our pizzeria, the decorator function will take any recipe function as input - for example, Hawaiian - and decorate it with the repetitive tasks, like rolling the dough and putting it in the pizza oven.
Whenever you need to add the functionality that got moved to the decorator, pass the function into the decorator, and the return value will be decorated with the extra functionality!
Here’s a simple example of a decorator written out in Python:
def my_decorator(function_to_wrap):def wrapper():print("Do something at the start")function_to_wrap()print("Do something at the end")return wrapperdef example():print("Do something special")example = my_decorator(example)example()
So in our pizza example, the Hawaiian function will only contain the specific tasks for pineapple and ham, but the decorator function will add the other tasks like serving the pizza on a plate.
Let’s try applying the decorator pattern to our pizzeria!
The Decorator Syntax in Python
One unfortunate consequence of decorators is that you have to create a function and then redefine it with the decorator. It would be neater if you could do it all in one step.
Fortunately, this is possible, thanks to a rather unusual syntax:
def my_decorator(function_to_wrap):def wrapper():print("Do something at the start")function_to_wrap()print("Do something at the end")return wrapper@my_decoratordef example():print("Do something special")example()
@my_decorator tells the Python interpreter that example should be decorated by the decorator function my_decorator.
Let’s improve our pizzeria using this idea!
What else might I use a decorator for?
Let’s say your code was running slowly, and you wanted to add some timing and logging functionality to help you find which part is running slow.
You could write a decorator that starts and stops a timer and logs the result to the screen or a file!
Then for each function you wanted to test, you could write
@my_timing_decorator on the line before it, and voilà, you’ve written your own code analysis tool.
In Python, functions are first-class objects, so they can be passed into and out of functions just like any other variable.
The decorator design pattern provides a way to modify a function, often by adding functionality before and after it runs.
It can be useful when several similar functions have differing core functionality but significant shared functionality.
@decorator_functionsyntax makes it simpler to write code involving decorators.
Now that you’ve seen two smaller design patterns, meet me in the next chapter to explore how to structure an entire application using the model-view-controller design pattern.