Welcome to part 18 of the intermediate Python programming tutorial series. In this tutorial, we are going to be discussing decorators.
Decorators are a way for us to "wrap" a function inside another function, without actually hard-coding it to be like this every time. An example of this is Flask. When you go to create a new page, you simply define a new function, dictate what is returned, and then you use a decorator to give it an actual path, like:
@app.route('/contact/') def contact(): return render_template("contact.html")
All we had to do was describe what the page will do, then, to actually allow a browser to access it via a URL, and to handle actually returning data and all of that, we just use a decorator that comes with all of these things already.
We can use decorators on functions or methods as we see fit. Let's consider a simple example. Let's say we're writing a function that returns a brand new GPU.
def new_gpu(): return 'a new Tesla P100 GPU!'
If we do a print(new_gpu())
, we just get a new Tesla P100 GPU!
Right now, the function returns our new GPU, but it's actually a gift, so let's wrap it. To do this, we need to define our wrapping decoration:
def add_wrapping(item): def wrapped_item(): return 'a wrapped up box of {}'.format(str(item())) return wrapped_item
The add_wrapping
is going to be our decorator function, which takes a single parameter, which is item
. Whatever you want to actually wrap will go in here. When we wrap something, this just simply happens. Thus, when we "decorate" the new_gpu
function, that new_gpu
function will be the item
. Then, we have a new embedded function here that actually wraps the item (wrapped_item
). This function returns the wrapped version of the return of the item
that we passed. Then, back in the add_wrapping
function, we just return the wrapped_item
. Now, to wrap something, we can do: @add_wrapping
above it, like so:
@add_wrapping def new_gpu(): return 'a new Tesla P100 GPU!'
All together now:
def add_wrapping(item): def wrapped_item(): return 'a wrapped up box of {}'.format(str(item())) return wrapped_item @add_wrapping def new_gpu(): return 'a new Tesla P100 GPU!' print(new_gpu())
Output:
a wrapped up box of a new Tesla P100 GPU!
Got a bicycle?!
def new_bicycle(): return 'a new bicycle'
We can easily wrap that too:
@add_wrapping def new_bicycle(): return 'a new bicycle' print(new_bicycle())
Output:
a wrapped up box of a new bicycle
What if we...
@add_wrapping @add_wrapping def new_gpu(): return 'a new Tesla P100 GPU!'
You can chain decorators like this, and what you get is what you'd expect:
a wrapped up box of a wrapped up box of a new Tesla P100 GPU!
A few notes to make here, however. When we wrap a function, we're essentially overwriting it with new information, and we're losing some information. For example, with our gpu, we could reference its __name__
:
print(new_gpu.__name__)
Which gives us: wrapped_item
That may be what we wanted, or it might actually not. We may actually want to keep that original information instead. We can do:
from functools import wraps
Then use @wraps
over the wrapped item:
def add_wrapping(item): @wraps(item) def wrapped_item(): return 'a wrapped up box of {}'.format(str(item())) return wrapped_item
Then, if we do: print(new_gpu.__name__)
, we get new_gpu
.
What if we want to pass arguments, like we do with Flask or Django?
We can further nest the decorator:
def add_wrapping_with_style(style): def add_wrapping(item): @wraps(item) def wrapped_item(): return 'a {} wrapped up box of {}'.format(style,str(item())) return wrapped_item return add_wrapping
Notice that we've added a new parent function called add_wrapping_with_style
, containing everything else from before, and with the parameter of style
. Now, we can reference the style
parameter within the wrapped_item
function. Finally, from our new parent class, add_wrapping_with_style
, we can return add_wrapping
. Now, we can do something like:
@add_wrapping_with_style('beautifully') def new_gpu(): return 'a new Tesla P100 GPU!' print(new_gpu())
Output:
a beautifully wrapped up box of a new Tesla P100 GPU!
Now chaining can start to have some value:
@add_wrapping_with_style('horribly') @add_wrapping_with_style('beautifully') def new_gpu(): return 'a new Tesla P100 GPU!' print(new_gpu())
a horribly wrapped up box of a beautifully wrapped up box of a new Tesla P100 GPU!