** This is an optional lecture on a more intermediate/advanced topic of decorators. Feel free to skip if you are not interested at this time. This is a useful topic for people who plan to use Web Development libraries with Python, such as Flask or Django.**
Imagine the following function:
def my_func():
print("hello")
my_func()
hello
What if we wanted to add some more functionality to it, instead of just the print('hello') that is already does. Right now, the only way we know how to do this is to manually rewrite the function to add it in. But wouldn't it be nice if we could "decorate" this function with some more "functionality"
def my_func():
# Add more functionality here
# Like More print() statements or logic
# Then have the original function
print("hello")
# Can even add more functionality after original operations
In order to fill in the commented areas above in the function my_func, we can use the @ operator to attach a decorator. However we will need to create our own decorators (not very common for beginners in Python, but its pretty common to use a library that uses Decorators).
Decorators can be thought of as functions which modify the functionality of another function. They help to make your code shorter and more "Pythonic".
To properly explain decorators we will slowly build up from functions. Make sure to restart the Python and the Notebooks for this lecture to look the same on your own computer. So lets break down the steps:
def func():
return 1
func()
1
Remember from the nested statements lecture that Python uses Scope to know what a label is referring to. For example:
s = 'Global Variable'
def func():
print(locals())
Remember that Python functions create a new scope, meaning the function has its own namespace to find variable names when they are mentioned within the function. We can check for local variables and global variables with the local() and globals() functions. For example:
print(globals())
{'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', '__package__': None, '__loader__': None, '__spec__': None, '__builtin__': <module 'builtins' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '_ih': ['', "s = 'Global Variable'\n\ndef func():\n print(locals())", 'print(globals())'], '_oh': {}, '_dh': ['C:\\Users\\Marcial\\Pierian-Data-Courses\\Python-Narrative-Journey\\07-Final-Recruitment-Topics'], '_sh': <module 'IPython.core.shadowns' from 'C:\\Users\\Marcial\\Anaconda3\\lib\\site-packages\\IPython\\core\\shadowns.py'>, 'In': ['', "s = 'Global Variable'\n\ndef func():\n print(locals())", 'print(globals())'], 'Out': {}, 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x000001F83BA55390>>, 'exit': <IPython.core.autocall.ZMQExitAutocall object at 0x000001F83BAB7710>, 'quit': <IPython.core.autocall.ZMQExitAutocall object at 0x000001F83BAB7710>, '_': '', '__': '', '___': '', '_i': "s = 'Global Variable'\n\ndef func():\n print(locals())", '_ii': '', '_iii': '', '_i1': "s = 'Global Variable'\n\ndef func():\n print(locals())", 's': 'Global Variable', 'func': <function func at 0x000001F83BB73950>, '_i2': 'print(globals())'}
Here we get back a dictionary of all the global variables, many of them are predefined in Python. So let's go ahead and look at the keys:
print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__builtin__', '__builtins__', '_ih', '_oh', '_dh', '_sh', 'In', 'Out', 'get_ipython', 'exit', 'quit', '_', '__', '___', '_i', '_ii', '_iii', '_i1', 's', 'func', '_i2', '_i3'])
Note how s is there, the Global Variable we defined as a string:
globals()['s']
'Global Variable'
Now lets run our function to check for any local variables in the func() (there shouldn't be any)
func()
{}
Great! Now lets continue with building out the logic of what a decorator is. Remember that in Python everything is an object. That means functions are objects which can be assigned labels and passed into other functions. Lets start with some simple examples:
def hello(name='Jose'):
return 'Hello '+name
hello()
'Hello Jose'
Assign a label to the function. Note that e are not using parentheses here because we are not calling the function hello, instead we are just putting it into the greet variable.
greet = hello
greet
<function __main__.hello>
greet()
'Hello Jose'
This assignment is not attached to the original function:
del hello
hello()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-18-a803225a2f97> in <module>() ----> 1 hello() NameError: name 'hello' is not defined
greet()
'Hello Jose'
Great! So we've seen how we can treat functions as objects, now lets see how we can define functions inside of other functions:
def hello(name='Jose'):
print('The hello() function has been executed')
def greet():
return '\t This is inside the greet() function'
def welcome():
return "\t This is inside the welcome() function"
print(greet())
print(welcome())
print("Now we are back inside the hello() function")
hello()
The hello() function has been executed This is inside the greet() function This is inside the welcome() function Now we are back inside the hello() function
welcome()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-29-efaf77b113fd> in <module>() ----> 1 welcome() NameError: name 'welcome' is not defined
Note how due to scope, the welcome() function is not defined outside of the hello() function. Now lets learn about returning functions from within functions:
def hello(name='Jose'):
def greet():
return '\t This is inside the greet() function'
def welcome():
return "\t This is inside the welcome() function"
if name == 'Jose':
return greet
else:
return welcome
x = hello()
Now lets see what function is returned if we set x = hello(), note how the closed parenthesis means that name ahs been defined as Jose.
x
<function __main__.greet>
Great! Now we can see how x is pointing to the greet function inside of the hello function.
print(x())
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-4-c071f17c389d> in <module>() ----> 1 print(x()) NameError: name 'x' is not defined
Lets take a quick look at the code again.
In the if/else clause we are returning greet and welcome, not greet() and welcome().
This is because when you put a pair of parentheses after it, the function gets executed; whereas if you don’t put parenthesis after it, then it can be passed around and can be assigned to other variables without executing it.
When we write x = hello(), hello() gets executed and because the name is Jose by default, the function greet is returned. If we change the statement to x = hello(name = "Sam") then the welcome function will be returned. We can also do print(hello()) which outputs now you are in the greet() function.
Now lets see how we can pass functions as arguments into other functions:
def hello():
return 'Hi Jose!'
def other(func):
print('Other code would go here')
print(func())
other(hello)
Other code would go here Hi Jose!
Great! Note how we can pass the functions as objects and then use them within other functions. Now we can get started with writing our first decorator:
In the previous example we actually manually created a Decorator. Here we will modify it to make its use case clear:
def new_decorator(func):
def wrap_func():
print("Code would be here, before executing the func")
func()
print("Code here will execute after the func()")
return wrap_func
def func_needs_decorator():
print("This function is in need of a Decorator")
func_needs_decorator()
This function is in need of a Decorator
# Reassign func_needs_decorator
func_needs_decorator = new_decorator(func_needs_decorator)
func_needs_decorator()
Code would be here, before executing the func This function is in need of a Decorator Code here will execute after the func()
So what just happened here? A decorator simple wrapped the function and modified its behavior. Now lets understand how we can rewrite this code using the @ symbol, which is what Python uses for Decorators:
@new_decorator
def func_needs_decorator():
print("This function is in need of a Decorator")
func_needs_decorator()
Code would be here, before executing the func This function is in need of a Decorator Code here will execute after the func()
Great! You've now built a Decorator manually and then saw how we can use the @ symbol in Python to automate this and clean our code. You'll run into Decorators a lot if you begin using Python for Web Development, such as Flask or Django!