1. Real-world examples
You've learned a lot about how decorators work. This lesson will walk you through some real-world decorators so that you can start to recognize common decorator patterns.
2. Time a function
The timer() decorator runs the decorated function and then prints how long it took for the function to run. I usually wind up adding some version of this to all of my projects because it is a pretty easy way to figure out where your computational bottlenecks are.
All decorators have fairly similar-looking docstrings because they all take and return a single function. For brevity, I will only include the description of the function in the docstrings of the examples that follow.
3. Time a function
Like most decorators, we'll start off by defining a wrapper() function. This is the function that the decorator will return. wrapper() takes any number of positional and keyword arguments so that it can be used to decorate any function.
The first thing the new function will do is record the time that it was called with the time() function.
Then wrapper() gets the result of calling the decorated function. We don't return that value yet though.
After calling the decorated function, wrapper() checks the time again, and prints a message about how long it took to run the decorated function.
Once we've done that, we need to return the value that the decorated function calculated.
4. Using timer()
So if we decorate this simple sleep_n_seconds() function, you can see that sleeping for 5 seconds takes about 5 seconds, and sleeping for 10 seconds takes about 10 seconds.
This is a trivial use of the decorator to show it working, but it can be very useful for finding the slow parts of your code.
5. Memoizing
Memoizing is the process of storing the results of a function so that the next time the function is called with the same arguments; you can just look up the answer.
We start by setting up a dictionary that will map arguments to results.
Then, as usual, we create wrapper() to be the new decorated function that this decorator returns.
When the new function gets called, we check to see whether we've ever seen these arguments before.
If we haven't, we send them to the decorated function and store the result in the "cache" dictionary.
Now we can look up the return value quickly in a dictionary of results. The next time we call this function with those same arguments, the return value will already be in the dictionary.
6. Using memoize()
Here we are memoizing slow_function(). slow_function() simply returns the sum of its arguments. In order to simulate a slow function, we have it sleep for 5 seconds before returning.
If we call slow_function() with the arguments 3 and 4, it will sleep for 5 seconds and then return 7.
But if we call slow_function() with the arguments 3 and 4 again, it will immediately return 7. Because we've stored the answer in the cache, the decorated function doesn't even have to call the original slow_function() function.
7. When to use decorators
So when is it appropriate to use a decorator?
You should consider using a decorator when you want to add some common bit of code to multiple functions. We could have added timing code in the body of all three of these functions, but that would violate the principle of Don't Repeat Yourself. Adding a decorator is a better choice.
8. Let's practice!
Now that you've seen a few more decorators in action, you can try your hand at creating some in the following exercises.