A quick start for Python decorators

Synopsis

#!/usr/bin/env python3

import shutil

# Decorator which pre-checks the space in /tmp
# and throws an exception if the space is more than
# 50% used
def check_disk_space(check_path, threshold_percent):
    def inner_dec(f):
        def wrapper(*args, **kwargs):
            du = shutil.disk_usage(check_path)
            used_pct = (du.used / du.total) * 100
            if used_pct >= threshold_percent:
                raise Exception(f"Aborting call - {check_path} is >{threshold_percent} (={used_pct}) full")
            return f(*args, **kwargs)
        return wrapper
    return inner_dec

# Build another pre-set decorator
def check_tmp_over_50(f):
    return check_disk_space("/tmp", 50)(f)
# Use the decorator on some function that
# might need /tmp space
@check_disk_space('/tmp', 50)
def foo(a, b, c):
    print("Able to run foo - must have been disk space")

@check_tmp_over_50
def bar(a, b, c):
    print("Able to run bar - must have been disk space")
if __name__ == '__main__':
    try:
        foo(1,2,3)
        bar(1,2,3)
    except Exception as e:
        print(f'foo aborted with: {e}')

Getting Started

Decorator syntax and usage isn’t all that complicated – but at the moment you won’t find any help from the Python Tutorial (decorators aren’t mentioned in Defining Functions, nor in More on Defining Functions) and the Python Language Reference only really touches on the existence of decorators without much in the way of a detailed description in the Function definitions and Class definitions sections.

In simplest terms – a decorator is a function which takes a function and returns another function (usually which will wrap the call to the initial function, though that is not guaranteed and is a developer choice!).

The Synopsis above demonstrates the two main patterns:

Decorators without arguments

@check_tmp_over_50
def bar(a, b, c):
    ...

These decorators simply wrap a function – bar becomes the equivalent of

bar = check_tmp_over_50(bar)

Where the check_tmp_over_50 code will execute as defined (in this case before the call to bar is made.

Decorators with arguments

@check_disk_space('/tmp', 50)
def foo(a, b, c):
    ...

Decorators also allow a pattern with arguments – this is useful for cases like the above or anywhere the decorator logic needs to be configured on a per-decorate basis.

A decorator with arguments essentially defines a closure in which the actual decorator is returned – and the way the decorator with arguments is applied is first the outer decorator is called with the arguments given (which returns a function which you might think of as the actual decorator) and second the returned decorator is applied to the function:

So the above example is equivalent to:

foo = check_disk_space('/tmp', 50)(foo)

Or the explain that with descriptive code:

def decorator_with_args(foo, bar):
    def inner_decorator(f):
        # We are in the closure here with the values
        # of foo and bar now defined, so this inner_decorator
        # is written like a top-level no-argument decorator
        # except being able to use foo and bar
        def wrapper(*args, **kwargs):
            # And here we're in the wrapper - this thing
            # that is called on the way to calling f
            # ... run checks / do things ...
            return f(*args, **kwargs)
        return wrapper

    # Return the inner_decorator - this is what
    # is called and returned above just after
    #   check_disk_space('/tmp', 50)
    # Or to put it another way, the above function
    # is a decorator generator
    return inner_decorator

References

Leave a Reply

Your email address will not be published. Required fields are marked *