Introduction
Decorators in Python are used to extend an existing function without modifying that function's source code. This can be very useful to you as a developer as it allows additional functionalty to be built in before or after a function is run. Decorators can also be used to provide additional information about a function - for example a decorator can be used to perform debugging whereby the inputs and output of the function could be printed or logged.
A decorator is a function that takes another function as an input, wraps itself around that function to extend it, thereby providing additional functionality without explicitly modifying the original function.
A simple example may be to wrap a function that performs a complex calculation around a timer so that the time to execute that function may be determined. This could help determine where bottlenecks in your code are occuring. The calculation function would then be said to be decorated by the timer function - timing is determined without any modification to the original function.
This tutorial will walk you through the creation and implementation of your own decorators. There are also a number of decorators built in to Python which you may have already been exposed to (a commonly used decorator in classes is @property
). By following along in this tutorial you should gain a much better understanding of how decorators are constructed, and why we choose to use them rather than simply modifying the source code of the function we choose to decorate. Before we can jump to building our own, we'll first need to cover some basics of how functions operate in the Python environment.
Functions in Python - A Short Review
A function is a self-contained module of code that returns an output given some inputs. Functions are written so that common tasks in a program can be repeated or rerun without having to rewrite code. A very basic example would be a function to add two numbers together.
def sum2numbers(a,b):
"""Take two numbers a & b and add them"""
return a+b
Everytime sum2numbers
is called in a programme the two input numbers are summed and the result returned. Nice and simple.
>>> sum_result = sum2numbers(10,4)
>>> print(sum_result)
14
First-Class Objects
Functions in Python are First-Class Objects. While this may sound complicated, it simply means that they can be passed as a parameter, returned from a function, or assigned into a variable. Being first-class means that you can therefore write a function that takes another function as an input, and even return a function from within a function.
To issustrate the concept I have created a simple example where the function skycolor
, which determines the color of the sky based on whether it is cloudy or not, forms the input into the function todaytheskyis
which simply prints out the color of the sky.
def skycolor(clouds):
if clouds:
return 'grey'
else:
return 'blue'
def todaytheskyis(skycolor,clouds=False): # function skycolor is input to function todaytheskyis
color = skycolor(clouds)
return f"Hello World, today the sky is {color}!"
print(todaytheskyis(skycolor,True))
The output when clouds=True
is:
"Hello World, today the sky is grey!"
Nested Functions & Returning Functions
In Python it is possible to nest functions within a function. The functions within the outer or wrapper function are called inner functions. You can add multiple inner functions to a wrapper function and even return one of the functions as the result of the outer function. Depending on how the inner function is written, you can either execute the function before returning it, or alternatively return the unexecuted fuction, such that the variable assigned to the parent function becomes a function itelf, equal to the returned function.
In the example that follows you can pretend that we are going to a pet shop to choose a pet. We define the type of animal we are looking for and a name for the prospective pet. If the animal is in stock then we'll return with our new pet, and if not we'll return empty handed! Take note of the concepts illustrated:
- We have defined multiple inner functions in the
mypetchoice
function. - We are executing and returning a different function depending on the choice of pet made.
- When we run
mypetchoice
we are not actually returning a function but rather we evaluate the function and return the result.
def mypetchoice(animal,name):
def cat(name):
return f"Meet {name}, my new pet cat!"
def dog(name):
return f"My dog {name} loves to bark."
def notforsale(animal):
return f"Unfortunately there are no {animal}s for sale today!"
if animal == "cat":
return cat(name)
elif animal == "dog":
return dog(name)
else:
return notforsale(animal)
The outcome of our visit to the pet shop is dependent on our choice in animal.
>>> mypet = mypetchoice('cat','fluffy')
>>> print(mypet)
"Meet fluffy, my new pet cat!"
>>> mysisterspet = mypetchoice(animal='hamster',name='cuddles')
>>> print(mysisterspet)
"Unfortunately there are no hamsters for sale today!"
There is nothing stopping us from returning a function rather than evaluating the inner function without our parent. The example below shows how you can nest inner functions in a parent function and then return a reference to that inner function.
def animalsounds(animal):
def catsound():
return "Meowww"
def dogsound():
return "Woof"
if animal == 'cat':
return catsound
elif animal == 'dog':
return dogsound
else:
return None
Now evaluate animalsounds
, taking note that the output is a function.
>>> mycat = animalsounds('cat')
>>> print(mycat)
<function animalsounds.<locals>.catsound at 0x000001B882B30940> #ref to function
>>> print(mycat()) # execute the function by adding ()
"Meowww"
Decorator Syntax
Now that we have established that functions are first-class and can be nested within outer functions we are ready to move on to working with decorators. Remember that a decorator is simply a function that takes another function as an input, and extends the behaviour of that function without modifying it.
The basic synatx of a decorator is as follows:
def decorator_name(func): # function func is input attribute
def wrapper_decorator(*args,**kwargs):
print('This prints before the function is run')
# Do something before the function is run
value = func(*args,**kwargs) # Here the function is run
print('This prints after the function is run')
# do something after the function is run
return value # returns the value of the function func
return wrapper_decorator
The func
in the example above is the function that the decorator is wrapping up. value
is the result of the evaluated function which is returned and is accessible outside of the decorator. The *args
and **kwargs
passed to the func
ensures that all attributes are passed to the function inside the wrapper.
Since decorator_name
is just a function we can call it like any other function:
def hello():
print('hellooo!')
my_result = decorator_name(hello)
It almost all cases we'd rather use decorators in a more elegant way, calling them using the @ symbol. The preferred way to write out the example above is to place the decorator above the function you wish to decorate.
@decorator_name
def hello():
print('Hellooo')
First Practical Example - A Timer
The best way to come to grips with decorators is by going through a practical example. Let's say that we have written a function that determines the factorial of any number, and we would like to know how long it takes to calculate the result. We don't want to modify the factorial function so we can simply add a timer decorator to the function that calculates the function runtime.
The factorial of a whole number 'n' is simply defined as the product of that number with every whole number till 1.
$$ n! = n \times (n-1) \times (n-2) \times ... 3 \times 2 \times 1 $$
Our factorial function can be easily generated from the factorial
method found in the math
module.
import math
def calcfactorial(num):
return math.factorial(num)
Now we build the decorator which will take a timestamp before the factorial function is run and again just after it is run. Subtracting the difference will give us the runtime of the function.
import time # required to timstamp
def timer(func):
""" Evaluate the runtime of a function """
def wrapper_timer(*args,**kwargs):
start = time.perf_counter() # timestamp before function run
value = func(*args,**kwargs) # function is run now
end = time.perf_counter() # timestamp after function run
runtime = end - start # calculate function runtime
print(f"Function {func.__name__}({args[0]}) ran successfully in {runtime:4f} seconds.")
print(f"factorial({args[0]}) = {value}.")
return value # return the result of the function
return wrapper_timer
We are using time.perf_counter()
to timestamp the start and the end of the function run and then subtracting the end time from the start time to determine the runtime. To extract the runtime we only have to decorate the calcfactorial
function with our timer
function.
import math
@timer
def calcfactorial(num):
return math.factorial(num)
We can then run the factorial function with a few different inputs and compare the results.
>>> result = calcfactorial(6)
'Function calcfactorial(6) ran successfully in 0.000002 seconds.'
'factorial(6) = 720.'
>>> result = calcfactorial(62)
'Function calcfactorial(62) ran successfully in 0.000004 seconds.'
'factorial(62) = 31469973260387937525653122354950764088012280797258232192163168247821107200000000000000.'
Identity of Decorated Functions
Python functions are introspective. Introspection in a Python context refers to the ability of every python object (a function is an object) to know its attributes and methods at runtime.
If for example you wish to learn more about the methods and attributes found in the dict
class you can simply type help(dict)
in a python shell.
We can do the same for our own functions that we've created.
def sum2numbers(a,b):
""" Simply the sum of two numbers"""
return a+b
>>> print(sum2numbers.__name__)
'sum2numbers'
>>> print(help(sum2numbers))
"""
Help on function sum2numbers in module __main__:
sum2numbers(a, b)
Simply the sum of two numbers
None
"""
If we now add the @timer
decorator to sum2numbers
and call help
on our function we no longer see our sum2numbers
information but rather that of wrapper_timer
which is the wrapper def
inside which our sum2numbers
function is sitting.
@timer
def sum2numbers(a,b):
""" Simply the sum of two numbers"""
return a+b
>>> print(sum2numbers.__name__)
'wrapper_timer'
>>> print(help(sum2numbers))
"""
Help on function wrapper_timer in module __main__:
wrapper_timer(*args, **kwargs)
None
"""
This is easy to fix. All we have to do is add the @functools.wraps()
decorator into our own decorator before the wrapper function like so. Remember to import the functools
module first to use the decorator.
import functools
import time
def timer(func):
""" Get the runtime of a function """
@functools.wraps(func) # we have added this to fix our introspection
def wrapper_timer(*args,**kwargs):
start = time.perf_counter()
value = func(*args,**kwargs)
end = time.perf_counter()
runtime = end - start
print(f"Function {func.__name__}({args[0]}) ran successfully in {runtime:4f} seconds.")
print(f"factorial({args[0]}) = {value}.")
return value
return wrapper_timer
Now re-running our help function on sum2numbers
yields the expected result.
@timer
def sum2numbers(a,b):
""" Simply the sum of two numbers"""
return a+b
>>> print(sum2numbers.__name__)
'sum2numbers'
>>> print(help(sum2numbers))
"""
Help on function sum2numbers in module __main__:
sum2numbers(a, b)
Simply the sum of two numbers
None
"""
Reusing Decorators
We'd like to be able to reuse our decorators on multiple functions. The usual pythonic way of doing so is to create a module to house our decorators.
Create a file called decorators.py
to house your project decorators, and then import this module as required.
import decorators
# alternatively
from decorators import required_decorator
Two More Real World Examples
So far we have shown the usefulness of applying a timer decorator to our functions to determine the runtime of a function. We'll now discuss a two more useful applications, a debugger and a pause decorator.
A Debugger Decorator
A decorator is a smart way of providing debugging information to a function without modifying the function in any way. The example decorator shown here collects all the arguments and keyword arguments from the function, and prints these out before the function is called. After the function has been run the returned result of the function is then also printed to the screen. You could use the code below as a starting point from which to build a more powerful debugger.
import functools
def debugger(func):
""" A simple debugger decorator """
@functools.wraps(func)
def wrapper_debugger(*args,**kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{key}={val!r}" for key, val in kwargs.items()]
separator = ", "
signature = separator.join(args_repr + kwargs_repr)
print(f"Calling function: {func.__name__}({signature})")
value = func(*args,**kwargs)
print(f"Function {func.__name__!r} has run and returned {value!r}.")
return value
return wrapper_debugger
Let's see our debugger in action.
@debugger
def multiplier(a,b):
return a*b
ab = multiplier(6,5)
The multiplier
function is run and the two print statements in the debugger are printed to the screen.
"Calling function: multiplier(6, 5)"
"Function 'multiplier' has run and returned 30."
A Pause Decorator - Adding Arguments to our Decorators
There may be some instances when you'd like to pause a routine before allowing it to continue. One use case for this may be a method to reload a webpage after some elapsed time. Rather than hard-coding the pause duration into the decorator it would be far more useful to add a pause duration argument to the decorator so that we could vary the pause duration as required. What we then want is a decorator that looks something like this:
@pause(duration)
def myfunction():
...
return myresult
How then do we modify the decorator to include this additional duration argument? The trick is to add an additional def
that handles the argument. This def
is nested between the outer decorator function and the wrapper function we have been using up to now. The example below should make this a little clearer.
import functools
import time
def pause(delay=0): # note the additional nested function to accommodate the delay attribute
def pause_function(func):
@functools.wraps(func)
def wrapper_pause_function(*args,**kwargs):
if delay != 0:
print(f"Sleep for {delay} seconds before continuing.")
time.sleep(delay)
return func(*args,**kwargs)
return wrapper_pause_function
return pause_function
Add the decorator as normal but this time include a delay of 3 seconds.
@pause(3)
def multiplier(a,b):
return a*b
ab = multiplier(6,5)
The decorator will pause the routine for 3 seconds before continuing.
Wrapping Up
Decorators can often seem intimidating to a developer starting out. However, once you are comfortable with the syntax and understand the construction of the decorator you should quickly realize just how powerful decorators can be. This tutorial only serves as an introduction to the topic; in future tutorials we'll cover class-based decorators as well as the built in @property
decorator which is extremely useful when working in classes as it allows for getter
, setter
, and deleter
like properties to be built into your class design.
Summary Of What We Covered
- A decorator is a function that takes another function as an input, wraps itself around that function to extend it, providing additional functionality without explicitly modifying the original function.
- We call decorators by adding
@decorator
above the function we wish to decorate. - Arguments can be added to our decorator (for example to specify a pause duration). If arguments are added then our decorator syntax is modified slightly to include an additional nested
def
to handle the argument. - We include the
@functools.wraps()
decorator to our own decorator function to fix our function introspection. - Two decorator boiler plates are shown below: one without decorator arguments and one with. You can use these as a template to build your own.
Decorator without arguments
""" This is the basic syntax of a Python Decorator """
import functools
def example_decorator(func):
@functools.wraps(func)
def wrapper_example_decorator(*args,**kwargs):
# do something before calling the function
value = func(*args,**kwargs)
# do something after calling the function
return value # return the function
return wrapper_example_decorator
# To use the decorator you would add @example_decorator above the function you wish to decorate.
Decorator with arguments
""" Syntax of a decorator with arguments """
import functools
def decorator_name(attribute):
def decorator_function(func):
@functools.wraps(func)
def wrapper_decorator_function(*args,**kwargs):
# do something before calling the function
value = func(*args,**kwargs)
# do something after calling the function
return value
return wrapper_decorator_function
return decorator_function
"""
To use the decorator you would add @decorator_name(attribute)
above the function you wish to decorate.
"""