Python Decorators Explained For Beginners

What are Decorators?

Decorators are powerful tools that allow you to modify or enhance the functionality of a function by wrapping it in another function. The outer function is called the decorator and takes the original function as an argument and returns a modified version of the original without changing its source code.

Functions Recap

Before we dive into decorators, the following concepts are prerequisites to understanding how decorators behave. It is important to remember functions in Python are first-class objects, meaning that they are treated like other data types and can be used or passed as arguments.

Here are some examples of how functions work:

  1. Functions can take other functions as arguments:
def inner_function():
    print("inner_function is called")

def outer_function(func):
    print("outer_function is called")
     func()

outer_function(inner_function)
# outer_function is called
# inner_function is called
  1. Functions can define an inner function inside of them (nested function):
def outer_function():
    print("outer_function is called")

    def inner_function():
            print("inner_function is called")
  1. Functions can return a function as a value:
def outer_function():
    print("outer_function is called")

    def inner_function():
            print("inner_function is called")

    return inner_function

In this example, when we return "inner_function" we are not calling it. We are only returning a reference to it so that we can store and call it later on:

returned_function = outer_function()
# outer_funciton is called

returned_function()
# inner_function is called

How to create decorators

The previous concepts mentioned are all important parts of creating decorators. Decorators in Python are created using functions. You define a function that will act as your decorator, and this function takes another function (the one you want to decorate) as its argument. Inside the decorator function, you can modify the behavior of the original function by wrapping it inside of an inner function and then returning the inner function that includes the modifications.

Here's a step-by-step guide on how to create a decorator:

  1. Define the decorator function: Create a Python function that will serve as your decorator. This function should take another function as its argument.

  2. Define the wrapper function: Inside your decorator function, define an inner function (often called "wrapper"). This inner function can contain the modifications or additional behavior you want to add to the original function.

  3. Modify the behavior: Within the wrapper function, you can modify the arguments, return values, or add any extra functionality you desire.

  4. Return the wrapper function: At the end of your decorator function, return the wrapper function.

  5. Apply the decorator: Use the "@" symbol followed by the decorator function's name to apply it to the function you want to decorate.

Here's an example:

# Step 1: Define the decorator function
def my_decorator(func):
    # Step 2: Define the wrapper function
    def wrapper():
        #Step 3: Modify the functionality
        print("Before!")
        func()
        print("After!")

    # Step 4: Return the wrapper function
    return wrapper

# Step 5: Apply the decorator to a function
@my_decorator
def say_hello():
    print("Hello!")

# Call the decorated function
say_hello()

# Output:
    # Before!
    # Hello!
    # After!

In this example, "my_decorator" is the decorator function, "wrapper" is the inner function inside the decorator, and it modifies the behavior of the "say_hello" function when it's called.

When you run "say_hello", it prints messages from the decorator before and after the function's execution, which demonstrates how the decorator has modified its behavior.

By writing "@ my_decorator" above the "say_hello" function, it is equivalent to the following:

say_hello = my_decorator(say_hello)

Passing arguments to decorators

When using decorators there are times when we will want the decorated function to receive arguments when it is called from the wrapper function. To do this we need to pass down any arguments from our wrapper function to our original function.

Here's an example:

def divide_decorator(func):
    def inner(a, b):
        print(f"I am dividing {a} and {b}")

        return func(a, b)
    return inner

@divide_decorator
def divide(a, b):
    print(a/b)

divide(6, 2)
#Output:
    # I am dividing 6 and 2
    # 3.0

Here, when we call the "divide" function with the arguments (6, 2), the "inner" function defined in the "divide_decorator" decorator is called instead.

This "inner" function calls the original "divide" function with the arguments 6 and 2 and returns the result 3.0.

Why use decorators?

The primary reason to use decorators in your code is to reduce the amount of code that you need to write. If you find yourself repeating the same code in different functions and breaking the DRY "do not repeat yourself" principle, it is probably a good time to use decorators. Using decorators allows you to improve your code's readability, reusability, and maintainability.

Summary

In Python, functions are first-class objects. This means they can be treated like any other data type. They can be passed as arguments to other functions, defined inside of other functions, and returned by other functions. These concepts tie together to create decorators that allow us to extend the functionality of functions without modifying them directly. Decorators allow us to avoid repetitive code and make our applications more maintainable.

References

Chng, R. (2023, June 6). Python decorators explained for Beginners. freeCodeCamp.org. https://www.freecodecamp.org/news/python-decorators-explained/

Python decorators. Programiz. (n.d.). https://www.programiz.com/python-programming/decorator

GeeksforGeeks. (2023, January 23). Decorators in python. GeeksforGeeks. https://www.geeksforgeeks.org/decorators-in-python/