In this tutorial, you’ll learn how to use Python decorators on a function. Python decorators are useful tools to help you modify the behavior of a function. Similarly, decorators allow you to embrace the Don’t Repeat Yourself (DRY) programming principles for common operations you want to apply to multiple functions.
By the end of this tutorial, you’ll have a strong understanding of:
- How Python decorators work
- How to chain multiple Python decorators on a single function
- Some notes around ordering these decorators
Let’s get started!
Table of Contents
What are Python Decorators?
Python decorators are pieces of code that allow existing functions to be added to or modified when they are run, without modifying the underlying function definition. This is known as metaprogramming because the program itself attempts to modify another part of itself when the program is run.
We can think of decorators are syntactic sugar – meaning they may a more complicated execution of something else more syntactically pleasing. In order to dive briefly more into Python decorators, let’s look at function behavior.
Python functions are objects. This means that they can be assigned to other variables (among many other important things). Let’s take a look at an example of this.
def print_welcome():
print('Welcome to datagy!')
some_variable = print_welcome()
some_variable
# Returns: Welcome to datagy!
What we’ve done above is defined a function that welcomes you to the site. From there, we assigned that function to a variable some_variable
. Finally, we simply called that variable. This caused the function to be executed.
Now that we understand that Python functions are objects, we can do more than simply call them. We can even pass them into another function. What this means is that we can pass that function in and have it be returned.
Let’s see what this looks like:
def print_welcome():
print('Welcome to datagy!')
def print_hello(func):
def inner():
print('Hello!')
func()
return inner
decorated = print_hello(print_welcome)
decorated()
# Returns:
# Hello!
# Welcome to datagy!
The format above, however, is a bit awkward to read. This is where decorators come into play. We can replace the exact same example above by simply decorating our function, print_welcome()
, with the decorator function, print_hello()
. Let’s see what this looks like:
def print_hello(func):
def inner():
print('Hello!')
func()
return inner
@print_hello
def print_welcome():
print('Welcome to datagy!')
print_welcome()
# Returns:
# Hello!
# Welcome to datagy!
By doing this, we’re able to remove needing to explicitly pass a function into another function. Python decorators handle this implicitly.
Using Python Decorators to Modify Function Behaviour
In this section, let’s explore Python decorators from a more practical standpoint.
Using Python Decorators to Time Functions
In order to demonstrate their utility, let’s build a function that takes another function and times its execution. The benefit, here, of using a decorator is that it allows us to follow DRY (Don’t repeat yourself) programming principles. We can simply decorate multiple functions with these decorators to time their runtime.
import time
def timeit(func):
def timed():
start = time.time()
result = func()
end = time.time()
print(f'Program took {(end - start) * 1000}s to run')
return result
return timed
@timeit
def print_welcome():
print('Welcome to datagy!')
print_welcome()
# Returns:
# Welcome to datagy!
# Program took 0.0s to run
Let’s break down what we did above:
- We defined a function
timeit()
that takes another function - The function has another, inner function,
timed()
- The function tracks the start time, executes the decorated function, tracks the end time, calculates the difference and returns the result
- Finally, the outer function returns the inner function
When we apply this decorator function to our function, print_welcome()
, first our welcome greeting is returned and then the time of execution is display.
Using Python Decorators to Log Useful Info to the Terminal
Similar to the example above, we can use decorators to print useful information to the terminal when the program is run. For example, we may want to know which function is being run and the current time. This information can also be passed to a log file using the decorator, but we’ll focus only on printing it here.
Let’s see how we can create a decorator to accomplish this:
from datetime import datetime
def log_info(func):
def inner():
print(f'Starting run at {datetime.now()}')
print(f'Running {func.__name__}')
func()
return inner
@log_info
def print_welcome():
print('Welcome to datagy!')
print_welcome()
# Returns:
# Starting run at 2022-02-11 15:36:48.080846
# Running print_welcome
# Welcome to datagy!
In the example above, prior to running the function, our decorator prints the current date and time as well as the name of the function that will be run. This can be helpful if you’re running longer scripts and simply want to know where your program is at.
Passing Arguments into Python Decorators
So far, you’ve learned how to create some useful Python decorators. However, none of these decorators had arguments passed into them. In this section, you’ll learn how to create Python decorators that accept arguments.
In order to this, we’ll allow on Python syntactic magic unpacking. By this, I mean to use func_name(*args, **kwargs), which will unpack all arguments and all keyword arguments. By using this in your decorator, you ensure that the decorator will accept any number of arguments or keyword arguments. This makes them significantly more practical for repeated use.
Let’s take a look at an example:
def print_function_name(func):
def inner(*args, **kwargs):
print(f'Running {func.__name__}...')
return func(*args, **kwargs)
return inner
@print_function_name
def add_nums(a, b):
print(a + b)
add_nums(1, 2)
# Returns:
# Running add_nums...
# 3
The beauty of the above approach is that it accepts both positional and keyword arguments. Because of this, the function will run even if we execute it in any of the below formats:
add_nums(1, 2)
add_nums(1, b = 2)
add_nums(a = 1, b = 2)
Similarly, if we were to apply the decorator to another function, say one that took four arguments, the decorator would work without a problem. Let’s see what this looks like:
@print_function_name
def add_more_nums(a,b,c,d):
print(a + b + c + d)
add_more_nums(1, 2, 3, 4)
# Returns:
# Running add_more_nums...
# 10
In the next section, you’ll learn how to use multiple Python decorators for a single function.
Using Multiple Python Decorators
An interesting tidbit about Python decorators is that they can even be chained. This means that you can apply more than one decorator to a single function. In order to understand this, we’ll focus on a simple illustrative example to make the code easier to follow.
def one(func):
def inner(*args, **kwargs):
print('1')
return func(*args, **kwargs)
return inner
def two(func):
def inner(*args, **kwargs):
print('2')
return func(*args, **kwargs)
return inner
@one
@two
def speak(text):
print(text)
speak('Hello')
# Returns:
# 1
# 2
# Hello
The only things that our decorator functions do is print out the number 1 and the number 2. By placing the decorator @one
prior to @two
, you are wrapping the two()
wrapped function by one()
.
In order to illustrate this, you can switch the order to see how the behaviour is modified:
# Changing decorator order
@two
@one
def speak(text):
print(text)
speak('Hello')
# Returns:
# 2
# 1
# Hello
By placing the @two
decorator first, that function is made the outer most function.
Conclusion
In this tutorial, you learned about the Python decorators, which represents a syntactical sugar for metaprogramming. Python decorators allow us to modify function behavior and allow us to extend the functions in different ways.
You learned what decorators are and how to use them. You also learned how to allow parameters to be passed into Python decorators. Finally, you learned how to chain decorators and how the order of these decorators matters.
Additional Resources
To learn more about related topics, check out the tutorials below: