Often times in python there comes a need to have multiple functions act in a similar manner. It could be anything from making sure that similar functions output the right type of result, they all log when they are called or all report exceptions in the same way.
decorators An easy and repeatable way to accomplish this is . They look like:
@decorator
def my_function(print_string="Hello World"):
print(print_string)
my_function()
# Hello World
Decorators are simply wrapping a function with another function. In other words, it is exactly the same as doing:
def my_function(print_string="Hello World"):
print(print_string)
decorator(my_function)("My message")
# My message
So what does a decorator look like?
Decorator Template
from functools import wraps
def decorator(func):
"""This is an example decorators"""
@wraps(func)
def container(*args, **kwargs):
# perform actions before running contained function
output = func(*args, **kwargs)
# actions to run after contained function
return output
return container
Running the code above does nothing extra currently. It is showing how a decorator runs another function within itself.
@decorator
def my_function():
print "Hello World"
my_function()
# "Hello World"
Line by line breakdown
def decorator(func):
The name of the decorator function, which takes the wrapped function as its single argument.
@wraps(func)
Wraps is a built-in method that replaces the decorators name and docstring with that of the wrapped function, the section after the line by line breakdown explains why this is necessary.
def container(*args, **kwargs):
This inner function collects the parameters and keyword parameters that are going to be passed to the original function. This allows the decorator access to incoming arguments to verify or modify before the function is ever run.
output = func(*args, **kwargs)
This runs the function with the original arguments and captures the output. It is also possible to return a completely different result. It is more common to check or modify the output, like if you wanted to make sure everything returned was an integer.
return output
Don’t forget to actually return the original function’s output (or a custom one). Otherwise the function will simply return None
.
return container
The container function is the actual function being called, hence why *args, **kwargs
are passed to it. It is necessary to return
it from the outside decorator so it can be called.
The importance of Wraps
We need to incorporate wraps so that the function name and docstring appear to be from the wrapped function, and not those of the wrapper itself.
@decorator
def my_func():
"""Example function"""
return "Hello"
@decorator_no_wrap
def second_func():
"""Some awesome docstring"""
return "World"
help(my_func)
# Help on function my_func:
# my_func()
# Example function
help(second_func)
# Help on function container:
# container(*args, **kwargs)
It is possible, though more work to accomplish the same thing yourself.
def decorator(func):
"""This is an example decorators"""
def container(*args, **kwargs):
return func(*args, **kwargs)
container.__name__ = func.__name__
container.__doc__ = func.__doc__
return container
Useful Example
Now lets turn it into something useful. In this example we will make sure that the function returns the expected type of result. Otherwise it will raise an exception for us so there are not hidden complications down the road.
from functools import wraps
def isint(func):
"""
This decorator will make sure the resulting value
is a integer or else it will raise an exception.
"""
@wraps(func)
def container(*args, **kwargs):
output = func(*args, **kwargs)
if not isinstance(output, int):
raise TypeError("function did not return integer")
return output
return container
@isint
def add(num1, num2):
"""Add two numbers together and return the results"""
return num1 + num2
print(add(1, 2))
# 3
print(add("this", "that"))
# Type Error: function did not return integer
Regular decorators are already called on execution, so you do not need to add ()
s after their name, such as @isint
. However, if the decorator accepts arguments, aka a meta-decorator, it will require ()
even if nothing additional is passed to it.
Meta-decorators
Passing arguments to a decorator turns it into a Meta-decorator. To pass these arguments in, it requires either another function wrapped around the decorator or turn the entire thing into a class.
from functools import wraps
def istype(instance_type=int):
def decorator(func):
"""
This decorator will make sure the resulting value is the
type specified or else it will raise an exception.
"""
@wraps(func)
def container(*args, **kwargs):
output = func(*args, **kwargs)
if not isinstance(output, instance_type):
raise TypeError("function did not return proper type")
return output
return container
return decorator
@istype()
def add(num1, num2):
"""Add two numbers together and return the results"""
return num1 + num2
@istype(str)
def reverse(forward_string):
"""Reverse and return incoming string"""
return forward_string[::-1]
print(add(1, 2))
# 3
print(reverse("Hello"))
# "olleH"
print(add("this", "that"))
# Type Error: function did not return proper type
Remember running a decorator is equivalent to:
decorator(my_function)("My message")
Running a meta-decorator adds an additional layer.
reversed_string = istype(str)(reverse)("Reverse Me")
Hence why @decorator
doesn’t require to be called when put above a function, but @istype()
does.
You can also create this meta-decorator as a class instead of another function.
class IsType: # Instead of 'def istype'
def __init__(self, inc_type=int):
self.inc_type = inc_type
def __call__(self, func): # Replaces 'def decorator(func)'
@wraps(func)
def container(*args, **kwargs):
output = func(*args, **kwargs)
if not isinstance(output, self.inc_type):
raise TypeError("function did not return proper type")
return output
return container
In functionality they are the same, but be aware they are technically different types. This will really only impact code inspectors and those trying to manually navigate code, so it is not a huge issue, but is something to be aware of.
type(IsType)
<class 'type'>
type(istype)
<class 'function'>
Things to keep in mind
- Order of operation does matter. If you have multiple decorators around a single function keep in mind they are run from top to bottom
- Once a function is wrapped, it cannot be run without the wrapper.
- Meta-decorators require the additional parentheses, regular decorators do not.