Functions
Before you can understand decorators, you must first understand how functions work. Essentially, functions return a value based on the given arguments.
def foo(bar):
return bar + 1
print(foo(2) == 3)
First Class Objects
In Python, functions are first-class objects. This means that functions can be passed around, and used as arguments, just like any other value (e.g, string, int, float).
def foo(bar):
return bar + 1
print(foo)
print(foo(2))
print(type(foo))
def call_foo_with_arg(foo, arg):
return foo(arg)
print(call_foo_with_arg(foo, 3))
Nested Functions
Because of the first-class nature of functions in Python, you can define functions inside other functions. Such functions are called nested functions.
def parent():
print("Printing from the parent() function.")
def first_child():
return "Printing from the first_child() function."
def second_child():
return "Printing from the second_child() function."
print(first_child())
print(second_child())
What happens when you call theparent()
function? Think about this for a minute. You should get…
Printing from the parent() function.
Printing from the first_child() function.
Printing from the second_child() function
Try calling thefirst_child()
. You should get an error:
Traceback (most recent call last):
File "decorator03.py", line 15, in <module>
first_child()
NameError: name 'first_child' is not defined
What have we learned?
Whenever you callparent()
, the sibling functions,first_child()
andsecond_child()
are also called_AND_because of scope, both of the sibling functions are not available (e.g., cannot be called) outside of the parent function.
Returning Functions
Python also allows you toreturn functions from other functions. Let’s alter the previous function for this example.
def parent(num):
def first_child():
return "Printing from the first_child() function."
def second_child():
return "Printing from the second_child() function."
try:
assert num == 10
return first_child
except AssertionError:
return second_child
foo = parent(10)
bar = parent(11)
print(foo)
print(bar)
print(foo())
print(bar())
The output of the first two print statements is:
<function first_child at 0x1004a8c08>
<function second_child at 0x1004a8cf8>
This simply means thatfoo
points to thefirst_child()
function, whilebar
points to thesecond_child()
function.
The output of the second two functions confirms this:
Printing from the first_child() function.
Printing from the second_child() function.
Finally, did you notice that in example three, we executed the sibling functions within the parent functions - e.g,second_child()
. Meanwhile in this last example, we did not add parenthesis to the sibling functions -first_child
- upon returning so that way we can use them in the future. Make sense?
Now, my friend, you are ready to take on decorators!
Decorators
Let’s look at two examples …
Example 1
def my_decorator(some_function):
def wrapper():
print("Something is happening before some_function() is called.")
some_function()
print("Something is happening after some_function() is called.")
return wrapper
def just_some_function():
print("Wheee!")
just_some_function = my_decorator(just_some_function)
just_some_function()
Can you guess what the output will be? Try.
Something is happening before some_function() is called.
Wheee!
Something is happening after some_function() is called.
To understand what’s going on here, just look back at the four previous examples. We are literally just applying everything learned. Put simply, decorators wrap a function, modifying its behavior.
Let’s take it one step further and add an if statement.
Example 2
def my_decorator(some_function):
def wrapper():
num = 10
if num == 10:
print("Yes!")
else:
print("No!")
some_function()
print("Something is happening after some_function() is called.")
return wrapper
def just_some_function():
print("Wheee!")
just_some_function = my_decorator(just_some_function)
just_some_function()
This will output in:
Yes!
Wheee!
Something is happening after some_function() is called.
Syntactic Sugar!
Python allows you to simplify the calling of decorators using the@
symbol (this is called “pie” syntax).
Let’s create a module for our decorator
def my_decorator(some_function):
def wrapper():
num = 10
if num == 10:
print("Yes!")
else:
print("No!")
some_function()
print("Something is happening after some_function() is called.")
return wrapper
if __name__ == "__main__":
my_decorator()
Okay. Stay with me. Let’s look at how to call the function with the decorator:
from decorator07 import my_decorator
@my_decorator
def just_some_function():
print("Wheee!")
just_some_function()
When you run this example, you should get the same output as in the previous one:
Yes!
Wheee!
Something is happening after some_function() is called.
So,@my_decorator
is just an easier way of sayingjust_some_function = my_decorator(just_some_function)
. It’s how you apply a decorator to a function.
Real World Examples
How about a few real world examples …
import time
def timing_function(some_function):
"""
Outputs the time a function takes
to execute.
"""
def wrapper():
t1 = time.time()
some_function()
t2 = time.time()
return "Time it took to run the function: " + str((t2 - t1)) + "\n"
return wrapper
@timing_function
def my_function():
num_list = []
for num in (range(0, 10000)):
num_list.append(num)
print("\nSum of all the numbers: " + str((sum(num_list))))
print(my_function())
This returns the time before you runmy_function()
as well as the time after. Then we simply subtract the two to see how long it took to run the function.
Run it. Work through the code, line by line. Make sure you understand how it works.
from time import sleep
def sleep_decorator(function):
"""
Limits how fast the function is
called.
"""
def wrapper(*args, **kwargs):
sleep(2)
return function(*args, **kwargs)
return wrapper
@sleep_decorator
def print_number(num):
return num
print(print_number(222))
for num in range(1, 6):
print(print_number(num))
This decorator is used for rate limiting. Test it out.
One of the most used decorators in Python is thelogin_required()
decorator, which ensures that a user is logged in/properly authenticated before s/he can access a specific route (/secret
, in this case):
from functools import wraps
from flask import g, request, redirect, url_for
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
@app.route('/secret')
@login_required
def secret():
pass
Did you notice that the function gets passed to thefunctools.wraps()
decorator? This simplypreservesthe metadata of the wrapped function.
Let’s look at one last use case. Take a quick look at the followingFlaskroute handler:
@app.route('/grade', methods=['POST'])
def update_grade():
json_data = request.get_json()
if 'student_id' not in json_data:
abort(400)
# update database
return "success!"
Here we ensure that the keystudent_id
is part of the request. Although this validation works it really does not belong in the function itself. Plus, perhaps there are other routes that use the exact same validation. So, let’s keep it DRY and abstract out any unnecessary logic with a decorator.
from flask import Flask, request, abort
from functools import wraps
app = Flask(__name__)
def validate_json(*expected_args):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
json_object = request.get_json()
for expected_arg in expected_args:
if expected_arg not in json_object:
abort(400)
return func(*args, **kwargs)
return wrapper
return decorator
@app.route('/grade', methods=['POST'])
@validate_json('student_id')
def update_grade():
json_data = request.get_json()
print(json_data)
# update database
return "success!"
In the above code, the decorator takes a variable length list as an argument so that we can pass in as many string arguments as necessary, each representing a key used to validate the JSON data. Did you notice that this dynamically creates new decorators based on those strings? Test this out.