2. Decorators#
Decorators allow you to modify the behaviour of a callable. Decorators are themselves a callable that takes a callable as an input and return a callable. Let us build some decorator intuition through some simple examples.
Suppose we have a function that prints a string.
def my_func():
print("Function")
>>>my_func()
My function
Python treats functions as objects mean we can create another function that
accepts my_func
as an argument.
def wrapper(func):
print("Decorator")
return func
def my_func():
print("Function")
Above, we created a new function called wrapper
which accepts a
function as an argument, prints Decorator
then returns the function.
Remember functions are objects so we can assign the function returned from
wrapper
to a new variable.
>>>new_func = wrapper(my_func)
>>>new_func()
Decorator
Function
Above, we assign the callable object to the variable new_func
then we
call new_func
. This first prints Decorator
which is from the
wrapper
function, then it prints Function
which is from the
my_func
function. So we have used wrapper
to modify the
behaviour of my_func
without modifying my_func
itself.
Decorators essentially give us some syntax sugar to do this exact thing. In
fact wrapper
is a valid decorator.
def wrapper(func):
print("Decorator")
return func
@wrapper
def my_func():
print("Function")
>>>my_func
Decorator
Function
We have now decorated my_func
with the wrapper
decorator.
2.1. Passing Arguments to Decorators#
The arguments of decorated functions can be passed to the decorator. One reason you may want to do this is to validate the arguments passed to a function before the function is executed. Let us create a simple addition function and use a decorator to check the input arguments are integers.
def check_arguments(func):
def inner(a, b):
if isinstance(int, a) and isinstance(int, b):
return func(a, b)
else:
raise TypeError("Inputs must be integers")
return inner
@check_arguments
def add_integers(a: int, b: int) -> int:
return a + b
The decorator function check_arguments
contains a nested function
inner
takes the arguments from function being decorated. The function
check_arguments
then returns the callable inner
.
>>>add_integers(1, 2):
3
>>>add_integers("1", 2):
...
TypeError: Inputs must be integers
2.2. Example Decorators#
Here are some simple example decorators.
2.2.1. Check Dictionary Keys#
Suppose we have a function that takes a dictionary as an argument. We can create a decorator that makes sure the dictionary has the correct keys.
class CheckDictKeys:
def __init__(self, dict_arg_name, allowed_keys):
self.dict_arg_name = dict_arg_name
self.allowed_keys = allowed_keys
def __call__(func):
def inner(*args, **kwargs):
try:
dictionary = kwargs[self.dict_arg_name]
except KeyError:
arg_names = inspect.getfullargspec(func).args
arg_index = arg_names.index(self.dict_arg_name)
dictionary = args[arg_index]
missing_keys = [
key for key in dictionary.keys() if key not in self.allowed_keys
]
if missing_keys:
msg = f"Dictionary is missing these keys: {missing_keys}"
raise RuntimeError(msg)
return func(*args, **kwargs)
@CheckDictKeys("dictionary", ["key1", "key2", "key3"])
def add_dict_values(dictionary: dict):
total = 0
for value in dictionary.values():
total += value
return total
We have created a decorator called CheckDictKeys
that will check if a
named dictionary contains the allowed keys. If any of the allowed keys are
missing, a RuntimeError
is raised.
>>>add_dict_values({"key1": 1, "key2": 2, "key3": 3})
6
>>>add_dict_values({"key1": 1, "key2": 2})
...
RuntimeError: Dictionary is missing these keys: ['key3']