Many Pythonistas are familiar with using decorators, but far fewer understand what’s happening under the hood and can write their own. It takes a little effort to learn their subtleties but, once grasped, they’re a great tool for writing concise, elegant Python.
This post will briefly introduce the concept, start with a basic decorator implementation, then walk through a few more involved examples one by one.
What is a decorator
Decorators are most commonly used with the @decorator
syntax. You may have seen Python that looks something like these examples.
@app.route("/home") def home(): return render_template("index.html") @performance_analysis def foo(): pass @property def total_requests(self): return self._total_requests
To understand what a decorator does, we first have to take a step back and look at some of the things we can do with functions in Python.
def get_hello_function(punctuation): """Returns a hello world function, with or without punctuation.""" def hello_world(): print("hello world") def hello_world_punctuated(): print("Hello, world!") if punctuation: return hello_world_punctuated else: return hello_world if __name__ == '__main__': ready_to_call = get_hello_function(punctuation=True) ready_to_call() # "Hello, world!" is printed
In the above snippet, get_hello_function
returns a function. The returned function gets assigned and then called. This flexibility in the way functions can be used and manipulated is key to the operation of decorators.
As well as returning functions, we can also pass functions as arguments. In the example below, we wrap a function, adding a delay before it’s called.
from time import sleep def delayed_func(func): """Return a wrapper which delays `func` by 10 seconds.""" def wrapper(): print("Waiting for ten seconds...") sleep(10) # Call the function that was passed in func() return wrapper def print_phrase(): print("Fresh Hacks Every Day") if __name__ == '__main__': delayed_print_function = delayed_func(print_phrase) delayed_print_function()
This can feel a bit confusing at first, but we’re just defining a new function wrapper
, which sleeps before calling func
. It’s important to note that we haven’t changed the behaviour of func
itself, we’ve only returned a different function which calls func
after a delay.
When the code above is run, the following output is produced:
$ python decorator_test.py Waiting for ten seconds... Fresh Hacks Every Day
Let’s make it pretty
If you rummage around the internet for information on decorators, the phrase you’ll see again and again is “syntactic sugar”. This does a good job of explaining what decorators are: simply a shortcut to save typing and improve readability.
The @decorator
syntax makes it very easy to apply our wrapper to any function. We could re-write our delaying code above like this:
from time import sleepdef delayed_func(func):
...read moreSource:: Hackaday