Back to home

Python Decorators In Action main image

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 okeeffed/python-decorators-in-action.

Prerequisites

  1. Familiarity with Pipenv. See here for my post on Pipenv.
  2. Familiarity with JupyterLab. See here for my post on JupyterLab.
  3. ...

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:

  1. We create a higher-order function that accepts a function as an argument before_after_hof.
  2. We create a function hello that prints Hello.
  3. We pass our function hello to the higher-order function before_after_hof to create a new function higher_order_fn.
  4. We call our new higher-order function higher_order_fn.
  5. Our higher-order function runs the wrap_func closure function that we have returned from before_after_hof.
  6. This wrap_func calls print('BEFORE').
  7. 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.
  8. 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

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.