Python Decorators In Action

An overview on Python higher-order functions, decorators and some practical examples for when decorators can be used.

On this page

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

  1. "Python decorator to measure the execution time of methods"
  2. okeeffed/python-decorators-in-action
  3. Primer on Python decorators

Photo credit: orwhat