The Complete Guide to Python Decorators
Python decorators provide simple syntax for calling higher-order functions.
Decorators are functions that accept another function and extend the functionaly of the parent function without modifying it.
If this sounds confusing, don't worry. This article will explain everything you need to know about using decorators in Python.
Table of Contents
You can skip to a specific section of this Python decorator tutorial using the table of contents below:
- What are Python Decorators?
- Decorator Syntax
- Multiple Decorators in a Single Function
- Multiple Arguments in Decorator Functions
- Assigning Decorators for Classes
- Returning a Class from a Decorated Function
- Python Decorators in Debugging Code
- Final Thoughts
What are Python Decorators?
Python decorators are used in wrapping functions and classes with additional code blocks, essentially adding additional functionality without modifying the underlying function or class.
Python decorator is a function that takes another function as an argument to perform additional functionality and returns the result of the overall code-block. This is possible because in python functions are considered first-class objects enabling the functions to be passed as arguments in other functions. As decorators treat other functions as their data, this programming technique is called metaprogramming.
The following code block demonstrates how the python decorators can be used. In this instance, the function “calculatetotal” is used to calculate the total of two given values. We wrap this function within a function called “checkvalue” to limit the execution of the original function, only if one of the given values are equal or greater than two hundred.
# Original Function
def calculatetotal(val1, val2):
return val1 + val2
def checkvalue(func):
# Inner Function to Validate Values
def wrapper(val1, val2):
if val1 >= 200 or val2 >= 200:
# Call the Original Function
return func(val1,val2)
# Return the final result
return wrapper
cal = checkvalue(calculatetotal)
total = cal(210, 10)
print(total)
RESULT
The “checkvalue” function receives the “calculatetotal” function as the argument while the inner function called “wrapper” will receive the two values which will be passed to the “calculatetotal” function as arguments.
Why are Decorators used?
Decorators are created because of their modularity and explicit behaviour. Using decorators, additional functionality can be added to any existing function without having to modify the original function. As the user can explicitly apply decorators based on their specific needs, this reduces the repetition of code blocks and leads to reusable and a cleaner code structure.
Decorators are mainly used for the following use-cases.
Functional Addons
The main reason for using decorators is to add extra functionality to an existing function without having to do major modifications to the original function and preserving the function as a reusable object.
Data Sanitization
In instances where variables and methods can be affected by other functions within a programme, decorators can be placed upon a function to sanitize the data and return the intended output.
Function Registration
To allow multiple subsystems to communicate with each other, decorators can be used to register the function so that each subsystem can carry out communications without having to explicitly gather information of each subsystem.
Built-in Decorators
The Python standard library and other frameworks provide us with built-in decorators that can be used to modify the behaviour of a function. The commonly used built-in decorators are as follows
- @classmethod / @staticmethod These decorators are used to create a method without creating a new instance.
- @mock.path / @ mock.patch_object Derived from the mock module these decorators are used in unit testing.
- @loginrequired In Django python web framework @loginrequired is used for setting login privileges
- @task In the Celery module, where it is used in distributing tasks across threads and mechanisms @task decorator is used to identify a function as an asynchronous task.
Decorator Syntax
In python, the symbol @ also called the pie syntax can be used to assign a decorator function to the specified function. The next example is the same code used to filter values which are equal or greater than two hundred but written using the decorator syntax.
def checkvalue(func):
# Inner Function to Validate Values
def wrapper(val1, val2):
if val1 >= 200 or val2 >= 200:
# Call the Original Function
return func(val1,val2)
# Return the final result
return wrapper
# Using @ symbol to indicate the decorator
@checkvalue
def calculatetotal(val1, val2):
return val1 + val2
print(calculatetotal(210, 10))
RESULT
We use the @ symbol with the decorator function name, in this case, the “checkvalue” to indicate that the “calculatetotal” will be wrapped using the “checkvalue” function. This enables us to directly call the “calculatetotal” function to get the combined functionality. If the condition within the “checkvalue” function fails, it will result in a None type object returning.
print(calculatetotal(10, 10))
RESULT
Multiple Decorators in a Single Function
In python, multiple decorators can be applied to a single function. The main consideration when applying functions is the order in which the decorators should be assigned. The order in which Python applies decorators is from top to bottom.
Let us see how we can apply multiple decorators to a single function.
def makeupper(func):
# Function to make message Upper Case
def wrapper(msg):
uppermsg = msg
uppermsg = uppermsg.upper()
print(f"makeupper decorator conversion -> {uppermsg}")
return func(uppermsg)
return wrapper
def maketitle(func):
# Function to make message Title Case
def wrapper(msg):
titlemsg = msg
titlemsg = msg.title()
print(f"maketitle decorator conversion -> {titlemsg}")
return func(titlemsg)
return wrapper
# Assign Multiple Decorators
@makeupper
@maketitle
def printgreeting(message):
print(f"Final message -> {message}")
# Call the Function
printgreeting("hello world")
RESULT
In the above example, two decorator functions are created to convert a given string to uppercase, or title case and pass it to the original function. Then the decorators are assigned to the “printgreeting” function.
The execution order of the decorators is determined by the order in which we have defined the decorators. The “makeupper” decorator is called first, then the “maketitle” decorator is called resulting in the final output being in the Title case as it is the last value passed to the original function “printgreeting”.
To reverse the order in which the decorators will be applied, we can simply change the positions of the decorators as below. The result will be an uppercase message as it will be the final decorator that will be called.
# Assign Multiple Decorators
@maketitle
@makeupper
def printgreeting(message):
print(f"Final message -> {message}")
# Call the Function
printgreeting("hello world")
RESULT
Multiple Arguments in Decorator Functions
In the above functions, we have assigned decorators with single and multiple arguments. In addition to explicitly assigning arguments, the Python decorator functions can be defined to accept any number of arguments.
In the following code block, we will define a decorator function to accept any number of integer arguments and call a function to get the squared value of each number.
def getvalues(func):
def wrapper(*args):
results = []
for x in args:
x = int(x)
results.append(func(x))
return results
return wrapper
@getvalues
def makesqure(value):
value = value**2
print(value)
makesqure(6, 8, 10, 12, 15)
RESULT
Using Python decorator function “getvalues”, we are allowing multiple arguments to be passed to the “makesqure” without changing the original “makesqure” function. The *args variable is used to accept any number of arguments, and each value is passed to the “makesqure” function using a for loop.
Assigning Decorators for Classes
Decorators that can be assigned to Classes as Python Class is a callable object. This allows the programmers to maintain the state of a program while adding additional functionality. The below example will demonstrate how a simple class can be used as a decorator.
class Printcalculation():
def __init__(self, func):
self.function = func
def __call__(self, value):
if value > 10:
self.function(value)
else:
print("Enter a value greater than 10.")
@Printcalculation
def multiply(value):
totalval = value * 50
print(str(totalval))
multiply(15)
RESULT
Tthe class “Printcalculation” is called in as the decorator. The “multiply” function is modified by adding a condition in the decorator class. the “__call” function only executes the “multiply” function if the given value is greater than ten. The “\call” method is implemented to make the class callable and each time the class is called the code inside the “\call__” function is executed after the class is initiated.
If the given value is less than 10, it will print the message saying, “Enter a value greater than 10.”. This is demonstrated by calling the multiply function with five (5) as the argument.
@Printcalculation
def multiply(value):
totalval = value * 50
print(str(totalval))
multiply(5)
RESULT
Let us look at an advanced decorator example, where we use the Class to maintain the state of a given function.
import functools
import time
class Time_difference:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.time_diff = 0
def __call__(self, *args, **kwargs):
get_time = time.perf_counter()
time_difference = get_time - self.time_diff
self.time_diff = get_time
print(f"Function waited {time_difference:0.4f} seconds before executing {self.func.__name__!r}")
return self.func(*args, **kwargs)
@time_difference
def printgreeting(name):
print(str("hello ").upper() + str(name).title())
printgreeting("barry")
time.sleep(1)
printgreeting("jenifer")
time.sleep(2)
printgreeting("harry")
RESULT
In this programme, we use the “Timedifference” class to calculate the time between each execution of the “printgreeting” function. Using the functools module, we use the “updatewrapper” function to wrap the “printgreeting” function, and return the wrapper. In this instance, the wrapper is the “call” method.
In the “Time_differance” class we store the execution time. Each time the function is called the new time will be subtracted from the stored time to get the difference in time between each execution.
Returning a Class from a Decorated Function
In Python, both class and function are regarded as objects that enable us to return a class in a decorated function. In the below example, we create a code block to multiple all given values by 50.
Let us see how this is implemented. The main class “Multiply” is created with the following functions.
- multiply_val - to implement functionality within a subclass. The “call” method will reference this function when a new instance is created.
- function_use - describe the function
- random_number - generate a random number
Then the decorator function called “multiplyfunction” is created. Inside that, we create a subclass of class “Multiply” called “MultiplicationSub” and create the function “multiply_val” to multiply each given value by 50.
In this instance, when we call “multiply_values” function it will return the created instance of the subclass not the reference.
from random import randint
class Multiply:
def __call__(self, *args):
return self.multiply_val(*args)
def multiply_val(self, *args):
raise NotImplementedError('Subclass must implement `multiply_val`.')
def function_use(self):
string = "Each given value will be multiplied by 50"
return string
def random_number(self):
return randint(1000, 9999)
def multiply_function(func):
class Multiplication_Sub(Multiply):
def multiply_val(self, *args):
if args:
results = []
for x in args:
x = int(x)
results.append(func(x))
return results
else:
return func(*args)
return Multiplication_Sub()
@multiply_function
def multiply_values(value=0):
if value != 0:
return value * 50
elif value == 0:
return "No Value Provided"
# Call the function_use function
print(multiply_values.function_use())
# Call the main function
print(multiply_values(2, 1))
print(multiply_values())
print(multiply_values(2, 1, 4))
# Call the random function
print(multiply_values.random_number())
RESULT
Using the result set, we can identify that the returning object is a class. This enables us to call functions within the “Multiply” class, in addition to the main functionality of the function. In the above example, the first function call we are calling “functionuse” function within the “Multiply” class. However we are only referencing the “multiplyvalues” function. The same is true when calling the “random_number” function.
Python Decorators in Debugging Code
Programmers can use Python decorators as a debugging tool to understand the functionality of a written function. To demonstrate this, we will create a simple programme that converts the given value to a specified temperature unit.
from functools import wraps
def debug(func):
# Copy the original function details
@wraps(func)
def wrapper_debug(*args):
# Create a list of arguments given
args_list = [repr(a) for a in args]
# Print the Called Function
print(f"The Function Called ==> {func.__name__}({args_list})")
# Print Each Argument
arg_number = 0
for args_num in args_list:
print(f"Argument {arg_number} ==> {args_list[arg_number]}")
arg_number += 1
# Print the output
value = func(*args)
print(f"The Function {func.__name__!r} Returned ==> {value!r}")
return value
return wrapper_debug
@debug
def convert_to_temp(temp, unit):
unit = str(unit).upper()
if unit == "C":
result = int(round((9 * temp) / 5 + 32))
result = f"{temp} converted to Celsius is {result} C"
elif unit == "F":
result = int(round((temp - 32) * 5 / 9))
result = f"{temp} converted to Fahrenheit is {result} F"
else:
result = "Please provide C (Celsius) or F (Fahrenheit) as an option"
return result
print(convert_to_temp(5,"C"))
# Create a Line
print(f"\n"+"="*50+"\n")
print(convert_to_temp(5,"F"))
RESULT
In the above example, we use the decorator function debug to deconstruct the “converttotemp” function. Using the @wraps decorator from the functools module lets us carry over the original function name, docstring, argument list, etc… Thus, deconstructing each step of the function without modifying the original function allows programmers to better understand their functions and experiment with them.
Final Thoughts
In this article, you have gained an understanding of what are Python decorators, when to use decorators and their advantages in writing programmes. Python decorators provide an efficient way to add additional functionality to existing functions while keeping the original function unchanged. Using decorators increases the code readability and increases the overall reusability of code blocks.