Introduction to Python Decorators

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 details.

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 nam 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.

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.

Author image
Kenya
I am a programmer by profession. I am highly interested in Python, Java, Data Science and Machine learning. If you need help in any of these, don't hesitate to contact me: [email protected]