Python Decorators In Action
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
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
- We create a function
- We pass our function
helloto the higher-order function
before_after_hofto create a new function
- We call our new higher-order function
- Our higher-order function runs the
wrap_funcclosure function that we have returned from
- It then calls
funcwhich was pass as an argument - then argument we passed in this case was the
hellofunction and so running it will print
- Finally, we call
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
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):
- Error handling.
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.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.
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
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
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
1,200+ PEOPLE ALREADY JOINED ❤️️
Get fresh posts + news direct to your inbox.
No spam. We only send you relevant content.