Altcademy - a Forbes magazine logo Best Coding Bootcamp 2023

What is decorator in Python

Understanding Decorators in Python

Decorators are one of the powerful features in Python, but for beginners, they can seem like a bit of a mystery. If you've been learning Python, you might have come across this term and wondered what it's all about. In this post, we're going to unravel the concept of decorators in a way that's easy to grasp, even if you're new to programming.

The Basics: What is a Decorator?

Imagine you have a gift. To make it look nicer, you wrap it in decorative paper. You're not changing the gift inside, just adding something to it to make it special. In Python, a decorator is somewhat similar. It's a way to add new "wrapping" (functionality) to an existing function without modifying its structure.

In technical terms, a decorator is a function that takes another function as an argument, adds some kind of functionality, and then returns a new function with that added functionality.

First Steps: Functions Within Functions

To understand decorators, you first need to understand that in Python, functions are first-class citizens. This means they can be passed around and used as arguments just like any other object (like strings or numbers).

Here's a simple example of a function within another function:

def parent_function():
    print("I'm the parent function.")

    def child_function():
        print("I'm the child function.")

    child_function()

parent_function()

When you run this code, you'll see the output:

I'm the parent function.
I'm the child function.

The child_function is defined within the parent_function and only exists inside the parent function's scope. It's not accessible outside its parent function.

Taking it Further: Functions Returning Functions

Now, let's take it a step further. Functions can not only be defined inside other functions, but they can also return other functions.

def parent_function():

    def child_function():
        print("I'm the child function.")

    return child_function

new_function = parent_function()
new_function()

In this example, parent_function returns child_function, and we store that returned function in new_function. When we call new_function(), we get the output:

I'm the child function.

Adding a Layer: Functions as Arguments

Functions can also take other functions as arguments. This is a key concept in understanding decorators.

def parent_function(child_function):
    child_function()

def another_child_function():
    print("Hello from another child function!")

parent_function(another_child_function)

When this code runs, it prints:

Hello from another child function!

Here, another_child_function is passed to parent_function as an argument and is called within parent_function.

The Actual Decorator

Now that we've understood the basics, let's look at an actual decorator. A decorator is a function that wraps another function, enhancing it or changing its behavior.

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_hello():
    print("Hello!")

decorated_function = my_decorator(say_hello)
decorated_function()

This will output:

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

The my_decorator function is a decorator that takes a function as an argument and defines a wrapper function inside it. The wrapper function calls the original function and adds some print statements before and after its call.

The @ Symbol: Syntactic Sugar

Python provides a convenient way to apply decorators using the @ symbol, often referred to as "syntactic sugar". This allows us to decorate a function directly when we define it.

@my_decorator
def say_goodbye():
    print("Goodbye!")

say_goodbye()

This code does exactly the same thing as the previous example but uses the @ symbol to apply the my_decorator to say_goodbye. It's cleaner and more readable.

Real-world Example: Timing a Function

A practical use of decorators is timing how long a function takes to run. Here's a decorator that does just that:

import time

def timing_decorator(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print(f"Function took {end_time - start_time} seconds to complete.")
    return wrapper

@timing_decorator
def long_running_function():
    for _ in range(1000000):
        pass

long_running_function()

This decorator, when applied to a function, will print out the time it took for the function to execute.

Decorators with Arguments

Sometimes, you might want to pass arguments to the function being decorated. Here's how you can modify the decorator to accept arguments:

def decorator_with_arguments(func):
    def wrapper(*args, **kwargs):
        print("Arguments were passed to the function.")
        return func(*args, **kwargs)
    return wrapper

@decorator_with_arguments
def function_with_arguments(greeting, name):
    print(f"{greeting}, {name}!")

function_with_arguments("Hello", "Alice")

The *args and **kwargs syntax allows the wrapper function to accept any number of positional and keyword arguments, which are then passed to the function being decorated.

Intuition and Analogies

Think of decorators as custom stickers for your laptop. Your laptop functions perfectly well without them, but adding a sticker can give it a personal touch (additional functionality). Decorators work the same way; they add to your functions without changing their core purpose.

Conclusion

Decorators are a powerful tool in Python that allows you to modify the behavior of functions in a clean, readable way. They can be a bit tricky to wrap your head around at first, but once you understand the concept of functions as objects that can be passed around, returned, and taken as arguments, the rest falls into place.

Just like a skilled artist uses layers to create a beautiful painting, decorators in Python allow you to layer functionality onto your functions. They encourage code reusability and can make your code more modular and elegant. So next time you find yourself repeating the same code over multiple functions, consider whether a decorator could help you "wrap" up that functionality in a more efficient way. With practice, you'll find that decorators can be a gift that keeps on giving in your Python projects.