Familiarity with Pipenv. See here for my post on Pipenv.
Familiarity with JupyterLab. See here for my post on JupyterLab.
...
What are Python decorators?
Python decorators are a way to modify classes and functions at runtime.
They make use of higher-order functions, which are functions that accepts and/or returns another function.
To demonstrate an example of a higher-order function that print "before" and "after" a provided function, see the following code:
# py_decorators.py
def before_after_hof(func):
def wrap_func():
print('BEFORE')
func()
print('AFTER')
return wrap_func
def hello():
print('Hello')
# Create the higher order function by passing our function as an argument
higher_order_fn = before_after_hof(hello)
# Call our new higher order function.
higher_order_fn()
Running this code, we get:
$ python3 py_decorators.py
BEFORE
Hello
AFTER
You might have an idea of what is happening, but to solidify and review:
We create a higher-order function that accepts a function as an argument before_after_hof.
We create a function hello that prints Hello.
We pass our function hello to the higher-order function before_after_hof to create a new function higher_order_fn.
We call our new higher-order function higher_order_fn.
Our higher-order function runs the wrap_func closure function that we have returned from before_after_hof.
This wrap_func calls print('BEFORE').
It then calls func which was pass as an argument - then argument we passed in this case was the hello function and so running it will print Hello.
Finally, we call print('AFTER').
Higher-order functions enable us to augment the behavior of a function. They can also be used directly as a decorator!
Let's update our code to reflect this change:
# py_decorators.py
def before_after_hof(func):
def wrap_func():
print('BEFORE')
func()
print('AFTER')
return wrap_func
+ @before_after_hof
def hello():
print('Hello')
- # Create the higher order function by passing our function as an argument
- higher_order_fn = before_after_hof(hello)
- # Call our new higher order function.
- higher_order_fn()
+ # Call our hello function
+ hello()
Running this code, we can confirm that we get the same output:
$ python3 py_decorators.py
BEFORE
Hello
AFTER
Amazing! We have now augmented our function hello with the before_after_hof decorator.
But why would we want to do this? A few example are to "inject" certain common functionality into a function. Examples include (but are not limited to):
Logging.
Error handling.
Authorization.
Performance.
Let's run through an example of each to get a feel for this.
Decorators with error handling
Our first basic example will be a contrived version of error handling.
For example, you might have a 3rd party API that you want to use in your application to capture errors. We can implement this as a decorator like so:
from third_party_api import capture_error
def send_error_data(func):
def wrap_func():
try:
func()
except Exception as e:
capture_error(e)
# Again raise the exception
raise e
return wrap_func
@send_error_data
def fn_that_fails():
raise Exception("Sorry, this failed")
fn_that_fails()
In our contrived example, the higher-order function will capture the error with our third-party library and then raise it again (to be handled elsewhere).
Decorators with authorization
The following example demonstrates the running of a function based on a user property - namely their role type.
This particular example is quite contrived, although you can see how it can be used to implement a version of authorization for functions based on their role.
class User:
def __init__(self, role='user'):
self.role = role
def admin_only(func):
def check_is_admin(*args, **kwargs):
if (args[0].role == 'admin'):
func(*args, **kwargs)
else:
print('You are not an admin')
return check_is_admin
@admin_only
def delete_important_record(user):
print('Allowed')
user1 = User()
user2 = User('admin')
delete_important_record(user1) # You are not an admin
delete_important_record(user2) # Allowed
Running the following code will result in the following output:
$ python3 py_decorators.py
You are not an admin
Allowed
You may also notice that in this example we needed to pass arguments. We can use the *args, **kwargs parameters to pass in any number of arguments and keyword arguments to our decorator.
import time
def timeit(method):
def timed(*args, **kw):
ts = time.time()
result = method(*args, **kw)
te = time.time()
if 'log_time' in kw:
name = kw.get('log_name', method.__name__.upper())
kw['log_time'][name] = int((te - ts) * 1000)
else:
print('%r %2.2f ms' % (method.__name__, (te - ts) * 1000))
return result
return timed
@timeit
def hello():
print("Hello World!")
hello()
Now we can use the @timeit decorator to profile the performance of a function call on the machine running the code:
$ python3 temp/py_decorators.py
Hello World!
'hello' 0.04 ms
Summary
Today's post covered an overview on decorators and higher-order functions. We have also covered the use of decorators with a number of examples including error handling, authorization and performance profiling.
Decorators are a useful feature that you'll see used across the frameworks and libraries in Python.