Decorators in python
Decorators in python are really hard to understand. This is a simple guide to using them that is hopefully instructive.
The most fundamental trick to understand them is knowing this:
# given
def decorator(f):
# do things
return f
# this:
def func():
return
func = decorator(func)
# Is the same as:
@decorator
def func():
return
Put in words, a decorator is a way of modifying how a function is declared.
It has two essential components to it:
- It runs the code inside the decorator
- It replaces the function given with whatever value the decorator returns.
There are three use-cases that come as a result of this:
1) Performing an action once, when a function is declared
If you want to specify that something happens when a function is declared, the decorator is the right way to do it.
The best use-case is if you're "registering" functions. For instance, say you want to make a list of functions that get run by a single function, you could do the following:
registered_functions = []
def register(f):
registered_functions.append(f)
return f
@register
def print_1():
print(1)
@register
def print_2():
print(2)
@register
def print_3():
print(3)
def run_registered_functions():
for func in registered_functions:
func()
>>> run_registered_functions()
1
2
3
This is contrived, obviously, but I have previously used this for things like:
- When I'm writing a module that people can import from, you can use this to choose which functions can be imported with
from my_module import *
- When I'm writing tests with pytest, you use decorators to mark tests that should be skipped.
2) Modifying the behaviour of a function.
Because you can change what function is returned, you can modify how a function works with decorators. The simplest example of this is:
def new_function():
print(2)
def decorator(f):
return new_function
@decorator
def original_function():
print(1)
>>> original_function():
2
See how even though I'm calling original_function
, it's actually executing new_function
, since the decorator returned that instead.
This isn't particularly useful. It's much better to "wrap" the function you're given:
def decorator(f):
def wrapper(*args, **kwargs):
# note, this wrapper will work with any function
# since it takes any arguments
print("about to call this function")
return_value = f(*args, **kwargs)
if return_value is None:
print("function did not return anything")
else:
print("function returned " + return_value)
return wrapper
@decorator
def just_return(value):
return value
>>> just_return(None)
about to call this function
function did not return anything
>>> just_return(2)
about to call this function
function returned 2
This is much more useful. You can do things like:
- Validate that a function returns something. Or that it returns a particular value (like, that it returns an integer)
-
Modify the return value. The best example of this is to make it so that a function is only run once, and any following calls to it return the same value.
3) Adding arguments to modify a function
One of the best perks of decorators is a feature that hasn't been demonstrated in this article: they can take arguments. That works like this:
def register(argument): def decorator(f): print("function decorated with argument: " + argument) return f return decorator
@register('the_argument') def my_func(): return
my_func() function decorated with argument the_argument
An example of where this is useful is when writing a web server. You might write a function that describes a particular page, and then register it with a decorator that gives its address on the server.
A note on wrappers
When wrapping a function, since a new function is returned, it loses the information about the function it wrapped.
This includes things like the function's name (which can be accessed with function.__name__
, but might say wrapper
rather than my_function_name
).
You can restore this by using the functools.wraps
decorator on the wrapper function.