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
- PEP 318 — Decorators for Functions and Methods : “This document is meant to describe the decorator syntax and the process that resulted in the decisions that were made.”
- PEP 3129 — Class Decorators : “This PEP proposes class decorators, an extension to the function and method decorators introduced in PEP 318.”
- Python wiki on PythonDecorators : “This page largely documents the history of the process of adding decorators to Python.”
- Python 3 Language Reference: (while accurate, a little dry and lacking in details – better to use the PEPs examples)
- Function definitions : includes decorator positioning
- Class definitions : includes decorator positioning
- decorator : glossary definition