< Logging Errors and Exceptions in Python | Contents
Decorators in Python¶
Purpose: To help you get comfortable with the topics outlined below.
Recomended Usage
- Run each of the cells (Shift+Enter) and edit them as necessary to solidify your understanding
- Do any of the exercises that are relevant to helping you understand the material
Topics Covered
- Decorators
Workbook Setup¶
The following magics will reload all modules before executing a new line and help make sure you follow PEP8 code style.
%load_ext autoreload
%autoreload 2
%load_ext pycodestyle_magic
%pycodestyle_on
import functools
Decorators¶
Decorators are wrappers that make code reuse easy. They are function/class wrappers that can be used to modify the input, output or even the function/class itself before execution.
@decorator
A bit more background on when and why decorators make sense to use¶
**Note:** Skip this section if you are comfortable with when/where/why you would use decorators.
If you are working with a codebase adding new features or getting it ready for production there is a good chance it will be useful to have a set of tools that profile how the code performs based on its application.
Maybe you will want to see how a particular function utilizes memory or the CPU. Maybe you have a set of custom debugging tasks that show you memory locations or other thing in a specifc way. Decorators allow you to do these things.
They allow you to just "decorate" the function/class with a little "@my_decorator" keyword above the function/class and viola! You can get a CPU profile, memory profile, memory locations of variables, show local and global vars and values, etc, etc. Whatever you write into the decorator you define.
As you can imaging, this can be a very powerful tool! Let's dive in!
Basic Decorators¶
Define my_decorator to take a function then defines an inner function that wraps that function
# Define a decorator
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
Now we can run a function with the decorator we just created
def say_hi():
print("hi")
greeting = my_decorator(say_hi)
greeting()
Instead of actually having to wrap the function inside our decorator like we did above
greeting = my_decorator(say_hi)
we can just use the @my_decorator notation to define a decorator (or wrapper) for a function.
# Use the decorator you defined to wrap a function
@my_decorator
def say_hi():
print("hi")
say_hi()
The big take home here is that even though all we did was call say_hi(), becuase it was decorated we were actually able to do extra stuff (in this case just print stuff before and after the function call but we'll do more interesting/useful stuff soon).
Decorators using functools¶
The above example of a decorator is fine if we don't pass any function arguments but as you can see below, it breaks if we try and pass an argument.
@my_decorator
def say_hi_2(name):
print("hi {}".format(name))
say_hi_2("james")
To fix this, we (ie preserve all information) we need to use a special decorator called @functools.wraps
def my_decorator2(func):
@functools.wraps(func)
def wrapper_my_decorator(*args, **kwargs):
print("Something is happening before the function is called.")
value = func(*args, **kwargs)
print("Something is happening after the function is called.")
return value
return wrapper_my_decorator
@my_decorator2
def say_hi_2(name):
print("hi {}".format(name))
say_hi_2("james")
Great! Now lets actually do some useful stuff with decorators
Practical Examples¶
Examples from https://realpython.com/primer-on-python-decorators
Timing using decorators¶
import time
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
@timer
def pass_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
pass_time(1)
pass_time(999)
Debugging using decorators¶
def debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
print(f"{func.__name__!r} returned {value!r}")
return value
return wrapper_debug
@debug
def make_greeting(name, age=None):
if age is None:
return f"Howdy {name}!"
else:
return f"Whoa {name}! {age} already, you are growing up!"
make_greeting("Richard", age=112)
make_greeting(name="Dorrisile", age=116)
Very decorative functions¶
Note: A function can be very decorative (ie. have multiple decorators):
@decorator_1
@decorator_2
@decorator_3
def hello():
print('hello')
Troubleshooting Tips¶
If you run into issues running any of the code in this notebook, check your version of Python and Jupyter against mine below
import sys
print(sys.version)
3.7.6 (default, Dec 31 2019, 17:12:14)
[Clang 11.0.0 (clang-1100.0.33.16)]
!jupyter --version
jupyter core : 4.6.1
jupyter-notebook : 6.0.2
qtconsole : not installed
ipython : 7.9.0
ipykernel : 5.1.3
jupyter client : 5.3.4
jupyter lab : 1.2.3
nbconvert : 5.6.1
ipywidgets : not installed
nbformat : 4.4.0
traitlets : 4.3.3
# import sys
# print(sys.version)
# !jupyter --version