Decorators

Jon Reades

A Basic Function

def hello():
  return('hello world')

print(hello())
hello world

Modifying the Function

Let’s get meta:

def better():
  def hello():
    print("  + Defining hello!")
    return('hello world')
  print("+ Calling hello...")
  rv = hello().title() + "!"
  print(f"+ hello returned {rv}")
  return(rv)

better()
+ Calling hello...
  + Defining hello!
+ hello returned Hello World!
'Hello World!'

Decorating the Function

def better(func):
  def wrapper():
    return func().title() + "!"
  return wrapper

@better
def hello():
  return('hello world')

print(hello())
Hello World!

WTF?

How did that happen?

def better(func):
  def wrapper():
    return func().title() + "!"
  return wrapper

@better
def hello():
  return('hello world')

print(hello())

The Decorator Function

Let’s unpack the wrapper:

def better(func):
  def wrapper():
    return func().title() + "!"
  return wrapper

@better
def hello():
  return('hello world')

print(hello())

Reusing a Decorator

Everything’s ‘better’ now:

@better
def goodbye():
  return('GooDBye world')

print(goodbye())
Goodbye World!

But…

But this:

@better
def bad_func():
  return(2)

print(bad_func())

Will trigger this:

      2 def wrapper():
----> 3   return func().title() + "!"

AttributeError: 'int' object has no attribute 'title'

Chaining Decorators1

def splitter(func):
  def wrapper():
    return func().split()
  return wrapper

@splitter
@better
def hello():
  return('hello world')

print(hello())
['Hello', 'World!']

Using Func(tion)Tools

And there are decorators for decorators…

from functools import wraps
def better(func):
  @wraps(func)
  def wrapper():
    return func().title() + "!"
  return wrapper

@better
def hello():
  return('hello world')

print(hello())
Hello World!

Unpicking Functools

from functools import wraps
def better(func):
  @wraps(func)
  def wrapper():
    return func().title() + "!"
  return wrapper
...

Making Use of Metadata

Compare:

def better(func):
  '''Better formatting of a string'''
  def wrapper():
    '''Wraps a function to format it.'''
    return func().title() + "!"
  return wrapper

@better
def hello():
  '''Prints hello world'''
  return('hello world')

print(hello.__name__)
print(hello.__doc__)
wrapper
Wraps a function to format it.
from functools import wraps
def better(func):
  @wraps(func)
  def wrapper():
    return func().title() + "!"
  return wrapper

@better
def hello():
  '''Prints hello world'''
  return('hello world')

print(hello.__name__)
print(hello.__doc__)
hello
Prints hello world

Some Applications 11

def simple_logger(func):
  def wrapper(*args, **kwargs):
    print(f"+ Executing '{func.__name__}' with args: {args}")
    result = func(*args, **kwargs)
    print(f"  + Result is: {result}")
    return result
  return wrapper

@simple_logger
def add(a,b):
  return a+b

add(2,5)
+ Executing 'add' with args: (2, 5)
  + Result is: 7
7

Some Applications 21

import atexit

# Register the exit_handler function
@atexit.register
def exit_handler():
    print("Exiting the program. Cleanup tasks can be performed here.")

# Rest of the program
def main():
    print("Inside the main function.")
    # Your program logic goes here.

if __name__ == "__main__":
    main()

This would output:

Inside the main function.
Exiting the program. Cleanup tasks can be performed here.

Benefits of Decorators1

  1. Code readability: each function is more narrowly focussed on the ‘thing’ it’s supposed to do, without extraneous validation, logging, or authentication ‘cruft’.
  2. Code reuse: keep your code ‘DRY’ (Don’t Repeat Yourself) by applying the same code across multiple functions (e.g. log the arguments this function received)
  3. Modification without alteration: extending the behaviour of something else (e.g. you can’t/don’t want to modify someone else’s code, but need some additional step to be performend)
  4. Logging made simple: add/remove debugging and logging functionality quickly, easily, and consistently.
  5. Development: Python web frameworks (Django, Flask) use decorators to handle requests.
  6. Error handling: manage error-handling ‘centrally’ by placing try and except around every function you want to manage.

There’s More…

We’ve barely scratched the surface, decorators can:

  • Take arguments (which might alter the behavour of the wrapped function).
  • Help to make classes and methods more useful (see the Methods and Classes lectures).
  • Manage common tasks like authorisation and permissions.

There’s lots, lost more, but using decorators effectively will seriously impress anyone interviewing you for a job while also helping you to understand a lot more about good programming!

Resources