You are currently viewing Decorators in Python: How They Work & Creating Custom Decorators

Decorators in Python: How They Work & Creating Custom Decorators

You might have encountered functions or classes decorated with functions prefixed with "@", for example, @random. These are known as decorators as they are placed above your class or function.

In this tutorial, you will learn about:

  • Decorators in Python
  • How to create a custom decorator
  • The working of a decorator and how it modifies the original function
  • How to create a custom decorator that accepts arguments and how it works
  • Applying multiple decorators on top of a function and how they operate on the original function

Decorator

What is a decorator in Python? A decorator is an advanced function in Python that modifies the original function without changing its source code. It offers a way to add functionality to existing functions.

If you want to create a class but don’t want to write the required magic methods (such as the __init__ method) inside it, you can use the @dataclass decorator on top of the class, and it will take care of the rest.

The @dataclass decorator adds the required magic method within the Pokemon class without changing the source code of the class.

This class works just like any other normal class that contains the __init__ method in Python, and you can access its attributes, and create methods.

By decorating @dataclass on top of the class eliminates the need to write the initializer method and other required methods within the class.

Creating a Custom Decorator

Although there are several pre-built decorator functions available, there is also a way to design a custom decorator function for a particular task.

Consider the following simple decorator function that logs a message on the console whenever the original function is called.

The log_message() function is a decorator function that accepts another function (func) as its argument. Inside log_message(), a wrapper() function is defined which prints a message and calls func.

The log_message() returns the wrapper, effectively changing the behaviour of the original function (func).

You can now create a normal function and decorate it with the log_message() decorator function.

Observe how the greet() function is decorated with the log_message() (@log_message) function. When decorating a function on top of another function, you have to stick to this convention.

When you run this code, you’ll get the following result.

Notice that the greet() function prints a simple message but the @log_message decorator altered the behaviour of the greet() function by adding a message before calling the original function. This modification happens while preserving the signature of the original function (greet()).

How Decorators Work?

Look at this part of the code from the above section where you used the @log_message to decorate the greet() function.

The above code is equivalent to the following expression.

You will obtain the same outcome as before if you execute the code after making the following modifications.

The greet() function is passed to the log_message() function and stored inside the greeting. In the next line, the greeting is called just like any other function. What is happening and how does it work?

After this line (greeting = log_message(greet)) is executed, the variable greeting points to the wrapper() returned by log_message(). If you print the variable greeting, you’ll get the reference of the wrapper() function.

This wrapper() function prints a message and has a reference to the greet() function as func and it calls this function within its own body to maintain the original functionality while adding extra behaviour.

Defining Decorator Without Inner Function

One may wonder why the code in the wrapper() function cannot be inserted inside the scope of the log_message() function like in the following code.

In the above code, the code inside the wrapper() function is now placed within the log_message() function’s scope. When you run the, you’ll see that the greet() function’s behaviour has changed but you get an error.

It says one argument is missing when you called the greet() function which means that the greet() function is now pointing to the log_message() function. But when you simply don’t call the greet function, it won’t throw any error.

There is little flexibility and very little you can do with it, yet in certain instances it will work.

Handling Function Arguments Within Decorator

What if you have a complex function that accepts arguments and processes them, then you can’t approach this problem in this way.

This code will result in an error as the log_message() function doesn’t have a nested inner function to handle the argument the greet() function accepts.

Defining Decorator With Inner Function to Handle Function Arguments

You can manage the arguments received by the greet() function by incorporating a nested function (wrapper()) within the log_message() decorator function, using *args and **kwargs as parameters.

This time, the code printed the argument ("Sachin") supplied to the greet() function when it was called, so you didn’t receive any errors.

The *args and **kwargs passed to the wrapper() is used to pass on the arguments to func (a reference for the original function) that enables the decorator function to handle the arguments accepted by the original function.

Returning Values from Decorator

In the example above, using greet("Sachin") resulted in the output. However, what if you wanted to return a value from the decorator?

Since your decorator @log_message doesn’t return a value directly, this code will return None.

To handle this situation, you need to ensure that the wrapper() function returns the return value of the original function.

When you run the following code, you’ll get the value returned by the greet() function.

Creating a Decorator that Accepts Argument

So far you’ve created simple decorators but decorators can also accept arguments. Consider the following decorator that accepts arguments.

In the above code, a decorator function slice_string() is defined. This (slice_string()) decorator function accepts three arguments: start (defaults to 0), end (defaults to 0), and step (defaults to None).

Within this (slice_string()) function, the inner function, slice_decorator(), takes another function (func) as an argument and within the slice_decorator() function, a wrapper function (slice_wrapper()) is defined.

The slice_wrapper() function takes any positional (*args) and keyword (**kwargs) arguments required to handle arguments if any accepted by the original function.

The slice_wrapper() function prints a simple message, and in the next line, checks if the argument is an empty string, if it is then a message is printed otherwise, the result is sliced from the specified range.

This slice_wrapper() function is returned by the slice_decorator() function and eventually, the slice_decorator() function is returned by the slice_string() function.

Now you can create a function and decorate @slice_string on top of it.

The intro() function is defined that takes text as an argument and returns it. Two arguments (2 and 7) are passed to the @slice_string decorator, meaning the text will be sliced from the character at index 2 to index 7 (excluding the character at the 7th index).

Overall, a decorator function that accepts arguments typically involves the interaction of three functions: the outer function (the decorator itself) that accepts arguments, an inner function (the wrapper) that receives the original function, and a nested function (the innermost wrapper) that modifies the behaviour of the original function.

Here is another example of a decorator that accepts an argument.

The @sleep_code decorator takes an argument t representing time in seconds. It modifies the behaviour of the original function (slow_down()) by delaying its execution using time.sleep(t) within the innermost function (sleep_wrapper()). Additionally, before returning the result, it prints the execution time taken by the code, which is measured using time.perf_counter().

When you run the code, you’ll get the following result.

Stacking Multiple Decorators on Top of a Function

So far you might have a pretty good idea about decorators and in this section, you’ll see that multiple decorator functions can be stacked on top of another function. Here’s a simple example.

Both decorator_1() and decorator_2() have the same boilerplate and log a simple message.

The log_message() is decorated with both (@decorator_1 and @decorator_2) decorators with the @decorator_1 being on the topmost level followed by the @decorator_2.

When you run this code, you’ll get the following result.

You can see that messages logged by the decorators are in the exact order as they are stacked on top of the log_message() function.

If you reverse the order of these decorators, the messages will be logged in the same order as well.

The code is equivalent to passing log_message() through decorator_1() first, and then passing the result (decorator_1(log_message)) through decorator_2().

Note: When you are stacking multiple decorators on top of the function, their order matters.

Practical Example

Here’s an example that shows when decorating a function with multiple decorators, they need to be in order.

When you run this code, the execution will delayed for 4 seconds because the sleep_code() will be invoked twice.

If you just reverse the order of the decorators in the above code, that would just work fine.

Output

You can observe the difference in the output in which the execution of the code took only 2 seconds. That’s why you need to ensure that the decorators are in the correct order above the function.

Conclusion

Decorators modify the behaviour of the original function without changing the source code of the original function. They are advanced functions that do modification while preserving the original function’s signature.

Python has several built-in decorator functions, and you can also create the custom decorator your program may need.

You saw when you define a custom decorator, you create a function returning a wrapper function. This wrapper function handles the modification and if your decorated function accepts arguments then it uses *args and **kwargs to pass on arguments. If the decorator function accepts arguments then you end up nesting the wrapper function into another function.

You also observed that in order to get the appropriate outcome, decorators must be stacked correctly on top of any function.


πŸ†Other articles you might be interested in if you liked this one

βœ…Why if __name__ == “__main__” is used in Python programs?

βœ…Serialize and deserialize Python objects using the pickle module.

βœ…Create a WebSocket server and client in Python.

βœ…Create and integrate MySQL database with Flask app using Python.

βœ…What is __getitem__ method in Python class?

βœ…What is the yield keyword in Python and how it is different from the return keyword?


That’s all for now

Keep Coding✌✌