Decorators in Python (with Examples)



In Python, decorators are a powerful tool that allow us to modify or enhance the behavior of functions or methods without changing their actual code. A decorator is essentially a function that takes another function as an argument, adds some kind of functionality, and then returns a new function.

Decorators in Python

Decorators are one of Python’s most powerful tools. It enables us to modify or enhance the behavior of functions or methods without permanently altering their code. They are widely used in frameworks like Flask and Django for tasks such as logging, authentication, and caching.

A decorator is essentially a higher-order function that takes another function as an argument, enhances or modifies it, and returns a new function with the added functionality. This allows us to extend or alter a function’s behavior without directly changing its source code. Think of a decorator as a wrapper around the original function.

Basic Syntax and Working of Decorator Function

A decorator is applied using the @decorator_name syntax above a function definition.

def shbytes_decorator(func):  # function takes another function as argument
    def wrapper():            # wrapper function which calls the argument function in it
        print("Do something before the function")
        func()                # argument function called
        print("Do something after the function")
    return wrapper

@shbytes_decorator            # @ decorator function 
def say_shbytes():            # function used as argument for decorator function
    print("shbytes.com")

say_shbytes()                 # call to the function, which calls decorator function
  • In this example, the @shbytes_decorator modifies the behavior of the say_shbytes function by adding extra print statements before and after it runs.
  • shbytes_decorator(func) is a decorator function which takes another function as an argument and returns a wrapper function

Working of Decorator Function

  • A function is passed to a decorator – The function that we want to extend is passed to the decorator. in our example, say_shbytes() is the function which is passed to the decorator shbytes_decorator(func).
  • Wrapper function – The decorator wraps the original function inside another function (wrapper in this case), which can add functionality. In our example wrapper() is the wrapper function.
  • Return a new function – The wrapper function is returned, replacing the original function with the new one that has the extended behavior.

Decorators and Functions with Arguments

Decorators can also accept arguments, making them more versatile.

# Decorators and Functions with Arguments
def repeat(num_times):        # decorator takes argument number of times function to called
    def decorator(func):      # decorator function takes function as an argument
        def wrapper(*args, **kwargs):   # wrapper function which takes arguments
            for _ in range(num_times):  # loop for the number of times
                func(*args, **kwargs)   # function with arguments called in loop
        return wrapper                  # return modified wrapper function
    return decorator

@repeat(num_times=3)            # @ decorator with num_times argument 3
def introduction(name):         # function with arguments, on which decorator is used
    print(f"Courses@{name}")

introduction("shbytes.com")     # call the actual function
  • In this example, the repeat decorator allows us to specify how many times the function should be called.
  • Function arguments are passed using the *args for any number of value arguments and **kwargs for any number of key-value pair arguments.

Program Output

Courses@shbytes.com
Courses@shbytes.com
Courses@shbytes.com

Chaining Multiple Decorators

We can apply more than one decorator to a function by stacking them.

# Chaining Multiple Decorators
def bold_decorator(func):          # decorator takes function as an argument
    def wrapper():                 # wrapper function to add <b> tag
        return f"<b>{func()}</b>"
    return wrapper                 # return wrapper function

def italic_decorator(func):        # decorator takes function as an argument
    def wrapper():                 # wrapper function to add <i> tag
        return f"<i>{func()}</i>"
    return wrapper                 # return wrapper function

@bold_decorator                    # bold_decorator applied on function
@italic_decorator                  # italic_decorator applied on function
def say_shbytes():                 # multiple decorators (chaining) applied on function
    return "shbytes.com"

print(say_shbytes())               # call to the actual function
# Output => <b><i>shbytes.com</i></b>
  • In this example, we have created two decorators – bold_decorator and italic_decorator.
  • Both decorators are applied on say_shbytes() function.
  • Order of decorators applied – The function say_shbytes is first passed to italic_decorator, and the result is then passed to bold_decorator.

Class-Based Decorators

Decorators can also be implemented using classes. This is useful when we need more state or functionality than simple functions can provide. This is achieved providing implementation to __call__ function. Class that implements the __call__ method, making instances of the class callable like a function.

# Class-Based Decorators
class ShbytesDecorator:         # decorator class
    def __init__(self, func):   # initialize function
        self.func = func

    def __call__(self):         # __call__ makes class instance callable like a function
        print("Before the function call")
        self.func()
        print("After the function call")

@ShbytesDecorator               # class decorator used over a function
def say_shbytes():
    print("shbytes.com")

say_shbytes()

In this example, ShbytesDecorator is a class that implements the __call__ method. It makes instances of the class callable like a function.

Built-in Python Decorators

Python includes several built-in decorators. Some common decorators in Python are:

@staticmethod

This is used on a method in a class. It makes the method static which can be called using the class itself. We don’t need the class object to call that method. It does not access or modify the class object (i.e., no self parameter).

class ShbytesClass:
    @staticmethod             # makes method static
    def say_shbytes():        # static method in Shbytes class
        print("shbytes.com")

ShbytesClass.say_shbytes()    # calling method using class name
# Output => shbytes.com

@classmethod

This also makes method a class level method and can be called using the class name. But this also provides features to update the class attributes, state of the class etc. It has access to self parameter.

class ShbytesClass:
    @classmethod              # makes method class level method
    def say_shbytes(cls):     # class level method
        print(f"Hello from {cls}")

ShbytesClass.say_shbytes()    # calling method using class name
# Output => Hello from <class '__main__.ShbytesClass'>

@property

Used to define getter, setter, and deleter methods in a class.

class MyClass:
    def __init__(self, value):
        self._value = value

    @property                # getter method for value property
    def value(self):
        return self._value

    @value.setter            # setter method for value property
    def value(self, new_value):
        self._value = new_value

obj = MyClass(10)   # calls __init__ method
print(obj.value)    # calls getter method for value property; Output => 10
obj.value = 20      # calls setter method for value property
print(obj.value)    # calls getter method for value property; Output => 20

Real-World Use Cases of Decorators

Decorators are extensively used in various Python frameworks for tasks like:

  1. Logging – Automatically log information about function calls.
  2. Authentication – Check whether a user is authenticated before executing a function.
  3. Caching – Cache results of expensive computations to avoid redundant work.
  4. Performance Timing – Measure the execution time of a function.

Logging Example

# logging example
def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args}, {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_function_call
def add(a, b):
    return a + b

print(add(5, 10))
# Output => Calling add with (5, 10), {}; 15

Performance Timing Example

# performance timing example
import time

def calculate_execution_time(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()        # take function start time
        result = func(*args, **kwargs)
        end_time = time.time()          # take function end time
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds to execute")
        return result
    return wrapper

@calculate_execution_time
def slow_function():
    time.sleep(1)
    print("Function finished")

slow_function()
# Output => Function finished; slow_function took 1.0010 seconds to execute

Summary

Python decorators provide a powerful and flexible way to enhance the functionality of functions or methods in our code. Whether we are using decorators for logging, authentication, performance tracking, or simply extending function behavior, decorators can help us write cleaner and more maintainable code.

In this article, we learned about Decorators in Python. Following topics were covered:

Code – Github Repository

All code snippets and programs for this article and for Python tutorial, can be accessed from Github repository – Comments and Docstring in Python.

Python Topics


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *