Python  

What are Decorators in Python?

📌 Introduction

In Python, decorators are powerful tools that let you add extra functionality to functions, methods, or classes without changing their original code. Think of them as wrappers. they “wrap” around an existing function and run extra code before or after the original function.

They’re widely used in real-world applications, especially in frameworks like Flask and Django, for tasks like logging, authentication, performance measurement, and access control.

⚙️ How Decorators Work

Python treats functions as first-class objects, meaning you can store them in variables, pass them as arguments, and return them from other functions. This flexibility allows decorators to work their magic.

A decorator:

  1. Takes a function (or class) as input.
  2. Creates a wrapper function that adds new behavior.
  3. Returns that wrapper instead of the original function.

Example:

def my_decorator(func):
    def wrapper():
        print("Before the function runs")
        func()
        print("After the function runs")
    return wrapper

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

say_hello()

Output:

Before the function runs
Hello!
After the function runs

Here, my_decorator is shorthand for:

say_hello = my_decorator(say_hello)

🧠 First-Class Functions & Higher-Order Functions

  • First-class functions: Functions can be stored in variables, passed as arguments, or returned from other functions.
  • Higher-order functions: Functions that take other functions as arguments or return them.

Example:

def apply(func, value):
    return func(value)

def square(x):
    return x * x

print(apply(square, 5))  # Output: 25

Decorators are higher-order functions because they take a function, modify it, and return a new one.

📚 Real-World Uses of Decorators

  • Logging: Record details each time a function runs.
  • Authentication: Check if a user is logged in.
  • Performance measurement: Track execution time.
  • Access control: Restrict certain functions.
  • Caching: Store results to avoid recomputation.

Example: Measuring Execution Time:

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Execution time: {end - start} seconds")
        return result
    return wrapper

@timing_decorator
def slow_function():
    time.sleep(2)
    print("Done!")

slow_function()

🛠 Advanced Decorators, with Arguments

Sometimes you need decorators that take their own arguments.

Example:

def repeat(num_times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Baibhav")

Output:

Hello, Baibhav!
Hello, Baibhav!
Hello, Baibhav!

🏛 Method Decorators (Inside Classes)

When decorating methods, remember the first parameter is always self.

Example:

def method_decorator(func):
    def wrapper(self, *args, **kwargs):
        print("Before method")
        result = func(self, *args, **kwargs)
        print("After method")
        return result
    return wrapper

class MyClass:
    @method_decorator
    def say_hi(self):
        print("Hi!")

obj = MyClass()
obj.say_hi()

🏷 Class Decorators

Decorators can also modify entire classes.

Example:

def add_class_name(cls):
    cls.class_name = cls.__name__
    return cls

@add_class_name
class Person:
    pass

print(Person.class_name)  

# Output: Person

⚡ Common Built-in Decorators

  • staticmethod: Defines a method that doesn’t use self.
  • classmethod: Defines a method that operates on the class itself.
  • property: Makes a method behave like an attribute (with optional setters/getters).

Example: property

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value >= 0:
            self._radius = value
        else:
            raise ValueError("Radius cannot be negative")

c = Circle(5)
print(c.radius)  # 5
c.radius = 10
print(c.radius)  # 10

🔗 Chaining Decorators

You can apply multiple decorators to the same function.

Example:

def decor1(func):
    def inner():
        return func() ** 2
    return inner

def decor2(func):
    def inner():
        return func() * 2
    return inner

@decor1
@decor2
def num():
    return 10

print(num())  

# Output: 400

📌 Summary

Python decorators are functions that modify or enhance other functions, methods, or classes without altering their original code. They rely on first-class functions and higher-order functions, allowing flexible, reusable, and clean code. From simple logging to complex argument-based decorators, they are widely used in real-world applications and frameworks. Decorators can be applied to functions, methods, and classes, and Python even provides built-in decorators like staticmethod, classmethod, and property. You can also chain multiple decorators for layered functionality, making them an essential tool for any Python developer.