Gyaan

Decorators

intermediate decorators wrapper functools

A decorator is simply a function that takes another function and extends its behavior. Think of it like wrapping a gift — the gift (original function) is the same, but now it has extra packaging (the decorator logic).

The Core Idea

Before we look at the @ syntax, let’s understand what’s actually happening.

Decorator Wrapping Flow
original_func
our function
decorator(original_func)
takes in original
wrapper_func
adds behavior + calls original
When we call original_func(), we're actually calling wrapper_func()
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function runs")
        result = func(*args, **kwargs)   # call the original
        print("After the function runs")
        return result
    return wrapper

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

# Manual decoration
say_hello = my_decorator(say_hello)
say_hello()
# Before the function runs
# Hello!
# After the function runs

The @ Syntax Sugar

The @decorator syntax is just a cleaner way to write func = decorator(func).

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

# This is EXACTLY the same as:
# say_hello = my_decorator(say_hello)

Always Use functools.wraps

Without functools.wraps, the wrapper replaces the original function’s name and docstring. This breaks introspection and debugging.

from functools import wraps

def my_decorator(func):
    @wraps(func)  # preserves func.__name__, func.__doc__, etc.
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

A Practical Example: Timing Decorator

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

slow_function()  # "slow_function took 1.0012s"

Decorators with Arguments

If we want our decorator to accept parameters, we need an extra layer of nesting.

def repeat(n):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

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

greet("Manish")  # prints "Hello, Manish!" three times

Stacking Decorators

We can apply multiple decorators. They run bottom to top (closest to the function first).

@decorator_a
@decorator_b
def my_func():
    pass

# Same as: my_func = decorator_a(decorator_b(my_func))

Common Built-in Decorators

  • @property — turns a method into a read-only attribute
  • @staticmethod — method that doesn’t need self or cls
  • @classmethod — method that receives the class (cls) instead of the instance
  • @functools.lru_cache — memoization (caches return values)
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

c = Circle(5)
c.area  # 78.53975 — accessed like an attribute, no parentheses

In simple language, a decorator wraps a function with extra behavior. The @ syntax is just a shortcut. And always use @wraps to keep the original function’s identity intact.