Decorators in Python were an advanced mystery to me when I first started out coding. I stayed away from them longer than was necessary, feeling like it was too advanced for me. Turns out decorators are nothing but syntactic sugar to call a higher-order functions on the decorated function. A higher-order function is roughly defined as a function that takes at least one other function as an argument, which returns a function, or both.
So let’s explore this abstract concept with an example I find really helpful: logging the arguments of a function. One way is to modify the function a function directly to log its input. But this is pretty cumbersome to do and potentially requires us to modify a lot of functions. Another way is to write a decorator for it!
Let’s start with a simple function we want to log:
def sum(a: int, b: int) -> int:
return a + b
To define a decorator we define a function that takes the decorated function as its first argument:
from typing import Callable
def logging_decorator(decorated_function: Callable):
pass
We then define a wrapper function inside our decorator that does the logging. Our wrapper function takes a generic list of arguments args
and a dictionary of keyword arguments kwargs
. Those, we print out (our logging) and then return our decorated_function
results by calling it with the given args
and kwargs
:
def logging_decorator(decorated_function: Callable) -> Callable:
def wrapper_function(*args, **kwargs):
print(f"arguments: {args}")
print(f"keyword arguments: {kwargs}")
return decorated_function(*args, **kwargs)
return wrapper_function
And that’s it. We can now decorate our sum
function with the logging decorator and run it:
@logging_decorator
def sum(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
sum(42, 10)
Which produces the following output:
arguments: (42, 10)
keyword arguments: {}
What we end up doing here is that we actually don’t directly call the sum
function, but rather we call the logging_decorator
function with our sum
function as its input argument decorated_function
. Calling the logging_decorator
function will now return our wrapper_function
, which will run our print statements and our decorated sum
function.
Improving the logging output
To make our logging decorator more useful we can add some bells and whistles to it. For example printing the name of the called function:
def logging_decorator(decorated_function: Callable) -> Callable:
def wrapper_function(*args, **kwargs):
print(f"function: {decorated_function.__name__}")
print(f"arguments: {args}")
print(f"keyword arguments: {kwargs}")
return decorated_function(*args, **kwargs)
return wrapper_function
Producing the output:
function: sum
arguments: (42, 10)
keyword arguments: {}
Or make it print out the function statement with all arguments:
def logging_decorator(decorated_function: Callable) -> Callable:
def wrapper_function(*args, **kwargs):
name = decorated_function.__name__
print(f"{name}({', '.join([str(arg) for arg in args])})")
return decorated_function(*args, **kwargs)
return wrapper_function
Producing the output:
sum(42, 10)
We can expand on this to also print out the keyword arguments of the decorated function and its return value. For that we actually run the decorated_function
inside our wrapper function and return the resulting value:
def logging_decorator(decorated_function: Callable) -> Callable:
def wrapper_function(*args, **kwargs):
name = decorated_function.__name__
arguments = ', '.join([str(arg) for arg in args])
keyword_arguments = ', '.join([f'{k}={v}' for k, v in kwargs.items()])
results = decorated_function(*args, **kwargs)
print(f"{name}({arguments}, {keyword_arguments}) -> {results}")
return results
return wrapper_function
To demonstrate I’ve added a keyword argument to the sum
function for an optional third summation parameter:
@logging_decorator
def sum(a: int, b: int, c: int = 0) -> int:
return a + b + c
if __name__ == "__main__":
sum(42, 10, c=9)
sum(0, 5, c=-4)
sum(9, 1)
Which will give us the following output:
sum(42, 10, c=9) -> 61
sum(0, 5, c=-4) -> 1
sum(9, 1, ) -> 10
There’s still a lot of room to improve on this, for example to actually use a logger
to log rather than just printing to the standard output. Another improvement would be to also log the types of the (keyword) arguments.
I hope this example of a logging decorator helped your understanding of how Python decorators work.