Decorators
Functions as Decorators
Decorators are a term used for functions that accept a function and return a new function.
For example this is a decorator:
def call_logger(func):
def wrapper(*args, **kwargs):
print("Function started")
value = func(*args, **kwargs)
print("Function returned")
return value
return wrapper
>>> def greet(name):
... print(f"Hello {name}")
...
>>> logged_greet = call_logger(greet)
>>> logged_greet("Trey")
Function started
Hello Trey
Function returned
That’s a bit of an odd example. Let’s look at a better one.
A Real Example
Let’s take our is_prime function:
def is_prime(candidate):
for n in range(2, candidate):
if candidate % n == 0:
return False
return True
And call it on a big number:
>>> is_prime(10000079)
True
Notice that it takes a little while to give us a result.
If we call it on the same number again, it takes just as long:
>>> is_prime(10000079)
True
Is there any way we could make it so the function remembers what input resulted in what output so it doesn’t need to re-compute everything each time?
Let’s write a decorator function that stores a cache of inputs and outputs of a given function and makes it faster to run it multiple times.
We’ll call our function memoize:
def memoize(func):
cache = {}
def new_func(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return new_func
So we’re using a dictionary to map inputs to outputs and we’re checking whether we have an input before we compute the output for it.
If we use this function on another function, we’ll see that the resulting function is only ever called once for each group of the same arguments.
>>> cached_is_prime = memoize(is_prime)
>>> cached_is_prime(10000079)
True
>>> cached_is_prime(10000079)
True
Notice it ran a lot faster that second time.
Decorator Syntax
So decorators are functions that accept a function as an argument and return a function as their return value. This is exactly what our memoize function does, so it is a decorator function.
Python has a shortened syntax for using decorators that allows us to wrap a function in a decorator right after we define it:
@memoize
def is_prime(candidate):
for n in range(2, candidate):
if candidate % n == 0:
return False
return True
This is syntactic sugar that causes Python to rebind the name is_prime to this equivalent:
def is_prime(candidate):
for n in range(2, candidate):
if candidate % n == 0:
return False
return True
is_prime = memoize(is_prime)
Let’s try it out:
>>> is_prime(40000003)
True
>>> is_prime(40000003)
True
>>> is_prime(399640200946009)
False
>>> is_prime(399640200946009)
False
Decorator Exercises
JSONify
This is the jsonify exercise in decorators.py.
Make a decorator jsonify that will take the return value of the function and return a JSON-encoded version of the return value.
>>> @jsonify
... def get_thing():
... return {'trey': "red", 'diane': "purple"}
...
>>> get_thing()
'{"diane": "purple", "trey": "red"}'
Hint
You’ll want to use json.dumps
Groot
This is the groot exercise in decorators.py.
Write a decorator that ignores the function it’s decorating and returns a function that prints “Groot”.
>>> @groot
... def greet(name):
... print(f"Hello {name}")
...
>>> greet("Trey")
Groot
Four
This is the four exercise in decorators.py.
Write a decorator that ignores the function it’s decorating and returns the number 4. In other words, it doesn’t return a new function like you might expect from a decorator.
>>> @four
... def greet(name):
... print(f"Hello {name}")
...
>>> greet
4
>>> type(greet)
<class 'int'>
Hide Errors
This is the hide_errors exercise in decorators.py.
Make a decorator hide_errors that will suppress all non-system exiting exceptions raised by the target function, returning None if an exception was caught.
The decorator should act like this:
>>> @hide_errors
... def divide(x, y):
... return x / y
...
>>> divide(1, 0)
>>> divide(1, 2)
0.5
Prompt on Error
This is the catch_all exercise in decorators.py.
Make a decorator catch_all that will catch all exceptions raised by a function, prompt the user to ask whether the program should be exited, and then act accordingly.
The decorator should act like this:
>>> @catch_all
... def divide(x, y):
... return x / y
...
>>> divide(1, 0)
Exception occurred: division by zero
Should we ignore this exception (Y/n)? y
>>> divide(1, 2)
0.5
>>> divide(1, 0)
Exception occurred: division by zero
Should we ignore this exception (Y/n)? n
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in decor
File "<stdin>", line 3, in divide
ZeroDivisionError: division by zero
Make Partial
This is the make_partial exercise in decorators.py.
Make a decorator make_partial which makes a function return a new partially-evaluated function.
Example:
>>> @make_partial
... def add(x, y):
... return x + y
...
>>> add(1)
functools.partial(<function add at 0x7f4d086b4950>, 1)
>>> add(1)(2)
3
>>> add(1)(3)
4
Hint
Use functools.partial.
Count Calls
This is the count_calls exercise in decorators.py.
Make a decorator count_calls that keeps track of the number of times a function is called and passes the call count in as the first argument to the function.
Example usage:
>>> @count_calls
... def add(count, x, y):
... print(f"called {count} times")
... return x + y
...
>>> add(1, 2)
called 1 times
3
>>> add(1, 2)
called 2 times
3
>>> add(1, 3)
called 3 times
4
Tip
You may need to use nonlocal when making the inner function in your decorator.
Decorator Helpers
Let’s take a closer look at the return value of our memoize decorator.
First we’ll use memoize to decorate a function:
@memoize
def divide(x, y):
"""Return the result of x divided by y."""
return x / y
Now let’s take a look at the resulting function:
>>> divide
<function memoize.<locals>.new_func at 0x7f4d06715378>
>>> divide.__doc__
>>> help(divide)
Whenever we decorate a function with our memoize decorator, the resulting function has a weird name and doesn’t have a docstring. That’s not good.
We can fix this by modifying our memoize function to retain the name and docstring of our original function.
The functools module in the standard library includes a wraps function that will help us with this task:
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def new_func(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return new_func
Now when we use memoize on our function we’ll see it looks just like it would have if we hadn’t used a decorator:
>>> @memoize
... def divide(x, y):
... """Return the result of x divided by y."""
... return x / y
...
>>> divide
<function divide at 0x7f4d05dda9d8>
>>> divide.__doc__
'Return the result of x divided by y.'
>>> help(divide)
There is a popular third-party library called wrapt which extends the functionality of wraps even further.
Decorator Classes
Decorators are functions that return functions. Put another way, decorators are callables that return callables.
Classes are callable. When you call a class you get an instance of that class. But class instances aren’t normally callable.
Is it possible to make a class instance callable?
Yes! To make a class instance callable, we need to implement __call__ on the class.
For example here’s a re-implementation of the call_logger decorator we made earlier, using a class:
class call_logger(object):
def __init__(self, function):
self.function = function
def __call__(self, *args, **kwargs):
print("Function started")
value = self.function(*args, **kwargs)
print("Function returned")
return value
>>> @call_logger
... def greet():
... """Greetings!"""
... print("Greetings!")
...
>>> greet()
Function started
Greetings!
Function returned