Decorators

Decorators are a special syntax to call a function with another function as an argument.

This is done with an @ symbol just before the definition of the function to be used as an argument for the function declared with the @ symbol. A comparison example is as follows:

def add(x, y):
	return x + y
add = timer(add)

@timer
def add(x, y):
	return x + y

Wrapper functions

Wrapper functions are the more complex pattern that decorators are used to implement. Wrapper functions are a way of extending a function and increasing it’s complexity without changing it.

This can be useful for code that cannot be changed without a large refactor, for keeping a codebase DRY, and for optionally implementing additional functionality across multiple similar functions.

An example of a wrapper function would be a profiling function that displays how long another function took to complete.

from time import time

def timer(function):

	def wrapper(*args, **kwargs)
		before = time()
		function_output = function(*args, **kwargs)
		after = time()
		print("Time taken: {}".format(after - before))
		return function_output

	return wrapper

Without decorators, this would be added to a function by redefining it with the following syntax:

def add(x, y):
	return x + y
add = timer(add)

However a decorator can be used to make this redefinition of the function more concise and readable:

@timer
def add(x, y):
	return x + y

Advanced use

Decorators can also be passed arguments, however this requires an additional wrapper function.

def run_multiple_times(times_to_run):

	def inner(function):

		def wrapper(*args, **kwargs):
			for _ in range(times_to_run):
				function_output = function(*args, **kwargs)
			return function_output

		return wrapper

	return inner

@run_multiple_times(4)
def add(x, y):
	return x + y

This increases complexity due to multiple nested wrapper functions, which makes code harder to read and maintain.