Python Decorators In Action
Published: Jan 16, 2022
Last updated: Jan 16, 2022
This is Day 34 of the #100DaysOfPython challenge.
This post will provide an overview Python decorators and then demonstrate their usage.
The final code can be found on my GitHub repo okeeffed/python-decorators-in-action
.
Prerequisites
- 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 printsHello
. - We pass our function
hello
to the higher-order functionbefore_after_hof
to create a new functionhigher_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 frombefore_after_hof
. - This
wrap_func
callsprint('BEFORE')
. - It then calls
func
which was pass as an argument - then argument we passed in this case was thehello
function and so running it will printHello
. - 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.
Performance
The last example comes directly from Fahim Sakri's article "Python decorator to measure the execution time of methods" with some modifications for a basic example of timing a hello
function:
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.
Resources and further reading
Photo credit: orwhat
Python Decorators In Action
Introduction