Introduction
In Python, a decorator is a design pattern that we can use to add new functionality to an already existing object without the need to modify its structure. A decorator should be called directly before the function that is to be extended. With decorators, you can modify the functionality of a method, a function, or a class dynamically without directly using subclasses. This is a good idea when you want to extend the functionality of a function that you don't want to directly modify. Decorator patterns can be implemented everywhere, but Python provides more expressive syntax and features for that.
In this article, we will be discussing Python decorators in detail.
How to Create Decorators
Let's see how decorators can be created in Python. As an example, we will create a decorator that we can use to convert a function's output string into lowercase. To do so, we need to create a decorator function and we need to define a wrapper inside it. Look at the following script:
def lowercase(func):
def wrapper():
func_ret = func()
change_to_lowercase = func_ret.lower()
return change_to_lowercase
return wrapper
In the script above, we have simply created a decorator named lowercase
that takes a function as its argument. To try out our lowercase
function we need to create a new function and then pass it to this decorator. Note that since functions are first-class in Python, you're able to assign the function to a variable or treat it as one. We shall employ this trick to call the decorator function:
def hello_function():
return 'HELLO WORLD'
decorate = lowercase(hello_function)
print(decorate())
Output
hello world
Note that you can merge the above two pieces of code into one. We created the function hello_function()
that returns the sentence "HELLO WORLD". Then we called the decorator and passed the name of this function as the argument while assigning it to the variable "decorate". When executed, you can then see that the resulting sentence was converted into lowercase.
However, there is an easier way of applying decorators in Python. We can simply add the @
symbol before the name of the decorator function just above the function to be decorated. For example:
@lowercase
def hello_function():
return 'HELLO WORLD'
print(hello_function())
Output
hello world
How to Apply Multiple Decorators to a Function
Python allows us to apply more than one decorator to a single function. In order to do this correctly, make sure that you apply the decorators in the same order that you'd run them as normal code. For example, consider the following decorator:
def split_sentence(func):
def wrapper():
func_ret = func()
output = func_ret.split()
return output
return wrapper
Here we have created a decorator that takes an input sentence and splits it into various parts. The decorator has been given the name split_sentence
. Let's now apply lowercase
and split_sentence
decorators to one function.
To run these operations in the correct order, apply them as follows:
@split_sentence
@lowercase
def hello_function():
return 'HELLO WORLD'
print(hello_function())
Output
['hello', 'world']
Our sentence has been split into two and converted into lowercase since we applied both lowercase
and split_sentence
decorators to hello_function
.
Passing Arguments to Decorator Functions
Python decorators can also intercept the arguments that are passed to the decorated functions. The arguments will in turn be passed to the decorated function at runtime. Consider the following example:
def my_decorator(func):
def my_wrapper(argument1, argument2):
print("The arguments are: {0}, {1}".format(argument1, argument2))
func(argument1, argument2)
return my_wrapper
@my_decorator
def names(firstName, secondName):
print("Your first and second names are {0} and {1} respectively".format(firstName, secondName))
print(names("Nicholas", "Samuel"))
Output
The arguments are: Nicholas, Samuel
Your first and second names are Nicholas and Samuel respectively
In the script above, the decorator accepts two arguments:, argument1
and argument1
.
Creating General Purpose Decorators
General purpose decorators can be applied to any function. These kinds of decorators are very helpful for debugging purposes, for example.
We can define them using the args
and **kwargs
arguments. All the positional and keyword arguments are stored in these two variables, respectively. With args
and kwargs
, we can pass any number of arguments during a function call. For example:
def my_decorator(func):
def my_wrapper(*args, **kwargs):
print('Positional arguments:', args)
print('Keyword arguments:', kwargs)
func(*args)
return my_wrapper
@my_decorator
def function_without_arguments():
print("No arguments")
function_without_arguments()
Output
Positional arguments: ()
Keyword arguments: {}
No arguments
As you can see, no arguments were passed to the decorator.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Now let us see how we can pass values to the positional arguments:
@my_decorator
def function_with_arguments(x, y, z):
print(x, y, z)
function_with_arguments(5, 15, 25)
Output
Positional arguments: (5, 15, 25)
Keyword arguments: {}
5 15 25
We have passed three positional arguments to the decorator. To pass keyword arguments, we have to use keywords in the function call. Here is an example:
@my_decorator
def passing_keyword_arguments():
print("Passing keyword arguments")
passing_keyword_arguments(firstName="Nicholas", secondName="Samuel")
Output
Positional arguments: ()
Keyword arguments: {'secondName': 'Samuel', 'firstName': 'Nicholas'}
Passing keyword arguments
Two keyword arguments were passed to the decorator.
In the next section, we will discuss how to debug decorators.
How to Debug Decorators
At this point, you must have seen that we use decorators to wrap functions. The wrapper closure hides the original function name, its parameter list, and docstring.
For example: If we attempt to get the metadata for the decorator function_with_arguments
, we will get the metadata of the wrapper closure. Let us demonstrate this:
function_with_arguments.__name__
Output
'my_wrapper'
This presents a great challenge during debugging. However, Python provides the functools.wraps
decorator that can help in solving this challenge. It works by copying the lost metadata to your decorated closure.
Now let us demonstrate how this works:
import functools
def lowercase(func):
@functools.wraps(func)
def my_wrapper():
return func().lower()
return my_wrapper
@lowercase
def hello_function():
"Saying hello"
return 'HELLO WORLD'
print(hello_function())
Output
hello world
Since we used functools.wraps
on the wrapper function, we can inspect the function metadata for "hello_function":
hello_function.__name__
Output
'hello_function'
hello_function.__doc__
Output
'Saying hello'
The above script clearly shows that the metadata is now referring to the function rather than to the wrapper. I recommend that you always use functools.wraps
anytime you are defining a decorator. This will make debugging much easier for you.
Conclusion
The purpose of decorators is to change the functionality of a class, method, or function dynamically without using subclasses directly or changing the source code of the class, method, or the function that we need to decorate. In this article, we saw how to create simple and general purpose decorators and how to pass arguments to the decorators. We also saw how to debug the decorators during development using the functools
module.