Python Decorators Demystified: From Basics to Advanced

Python Decorators Demystified: From Basics to Advanced

Decorators are one of Python’s most elegant features. They allow you to modify or enhance functions and classes without changing their source code.

Understanding the Basics

A decorator is simply a function that takes another function and extends its behavior without explicitly modifying it.

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

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

say_hello()
# Output:
# Before function call
# Hello!
# After function call

Decorators with Arguments

To handle functions with arguments, use *args and **kwargs:

def timer_decorator(func):
    import time

    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
        return result

    return wrapper

@timer_decorator
def calculate_sum(n):
    return sum(range(n))

result = calculate_sum(1000000)
# Output: calculate_sum took 0.0234 seconds
print(f"Result: {result}")

Preserving Function Metadata

Use functools.wraps to preserve the original function’s metadata:

from functools import wraps

def debug_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

@debug_decorator
def multiply(x, y):
    """Multiply two numbers"""
    return x * y

multiply(5, 3)
# Output:
# Calling multiply(5, 3)
# multiply returned 15

Decorators with Parameters

Create decorators that accept arguments:

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

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

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Class-Based Decorators

You can also create decorators using classes:

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count} of {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def process_data():
    print("Processing...")

process_data()  # Call 1
process_data()  # Call 2

Practical Example: API Rate Limiting

import time
from functools import wraps

def rate_limit(max_calls, time_window):
    calls = []

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            # Remove calls outside the time window
            calls[:] = [call for call in calls if call > now - time_window]

            if len(calls) >= max_calls:
                raise Exception(f"Rate limit exceeded: {max_calls} calls per {time_window}s")

            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(max_calls=3, time_window=60)
def api_call(endpoint):
    print(f"Calling API: {endpoint}")
    return {"status": "success"}

# Try making multiple calls
for i in range(5):
    try:
        api_call(f"/data/{i}")
    except Exception as e:
        print(f"Error: {e}")

Chaining Multiple Decorators

You can stack decorators on a single function:

@timer_decorator
@debug_decorator
@repeat(times=2)
def complex_operation(x):
    return x ** 2

complex_operation(5)

Conclusion

Decorators are powerful tools for writing cleaner, more maintainable Python code. They’re perfect for cross-cutting concerns like logging, timing, authentication, and caching.