foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • quiz
Python
  • Understand what decorators are and how they work
  • Create and apply function decorators
  • Work with decorator arguments and nested decorators
  • Apply decorators in real-world scenarios

Function Decorators

Introduction

Imagine you're a chef in a busy restaurant. Each dish that leaves your kitchen needs certain preparations - maybe it needs to be garnished, or served at a specific temperature, or plated in a particular way. Instead of remembering to do these things for each dish individually, you create a "presentation station" where all dishes pass through automatically.

In programming, decorators are like that presentation station. They allow you to add functionality to functions without modifying the functions themselves. This is one of Python's most powerful features for code reuse and organization.


What is a Decorator?

A decorator is a function that takes another function as input and returns a modified version of that function. It's like wrapping a gift - the original item is still there, but now it has extra packaging and features.

The Basic Concept

def my_decorator(func):
    def wrapper():
        print("Something before the function runs")
        func()
        print("Something after the function runs")
    return wrapper

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

# Manual decoration
decorated_hello = my_decorator(say_hello)
decorated_hello()
# Output:
# Something before the function runs
# Hello.
# Something after the function runs

The @ Syntax

The @ symbol is syntactic sugar that makes decorators easier to apply:

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

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

# Now say_hello is automatically decorated
say_hello()
# Output: Before -> Hello! -> After

Function Decorators in Action

Timing Decorator

import time

def timer(func):
    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:.2f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done!"

result = slow_function()
# Output: slow_function took 1.00 seconds

Logging Decorator

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper

@logger
def add_numbers(a, b):
    return a + b

result = add_numbers(5, 3)
# Output:
# Calling add_numbers with args: (5, 3), kwargs: {}
# add_numbers returned: 8

Authentication Decorator

def requires_auth(func):
    def wrapper(user, *args, **kwargs):
        if not hasattr(user, 'is_authenticated') or not user.is_authenticated:
            raise PermissionError("User must be authenticated")
        return func(user, *args, **kwargs)
    return wrapper

class User:
    def __init__(self, name, authenticated=False):
        self.name = name
        self.is_authenticated = authenticated

@requires_auth
def view_dashboard(user):
    return f"Welcome to dashboard, {user.name}!"

user1 = User("Alice", authenticated=True)
user2 = User("Bob", authenticated=False)

print(view_dashboard(user1))  # Works
# print(view_dashboard(user2))  # Raises PermissionError

Preserving Function Metadata

When you decorate a function, the original function's metadata (name, docstring, etc.) gets replaced. Use functools.wraps to preserve this information:

import functools

def my_decorator(func):
    @functools.wraps(func)  # Preserves original function metadata
    def wrapper(*args, **kwargs):
        """This is the wrapper's docstring"""
        print("Decorator is running")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def important_function():
    """This function does something very important."""
    return "Important result"

print(important_function.__name__)  # "important_function" (not "wrapper")
print(important_function.__doc__)   # "This function does something very important."

Decorators with Arguments

Sometimes you want your decorator to accept parameters:

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(times):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

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

results = greet("Alice")
print(results)  # ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']

How It Works

  1. @repeat(times=3) calls repeat(3), which returns a decorator
  2. The decorator is then applied to greet
  3. When greet is called, it runs 3 times

Multiple Decorators

You can stack multiple decorators on a single function:

@timer
@logger
def complex_calculation(x, y):
    return x ** y

result = complex_calculation(2, 10)
# Output:
# Calling complex_calculation with args: (2, 10), kwargs: {}
# complex_calculation took 0.00 seconds
# complex_calculation returned: 1024

Decorators are applied from bottom to top, so the execution order is:

  1. timer wraps the function
  2. logger wraps the timer-wrapped function

Real-World Applications

Caching Decorator

def cache(func):
    cache_dict = {}
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Create a cache key from arguments
        key = (args, tuple(sorted(kwargs.items())))
        
        if key in cache_dict:
            print(f"Cache hit for {func.__name__}")
            return cache_dict[key]
        
        print(f"Computing {func.__name__}")
        result = func(*args, **kwargs)
        cache_dict[key] = result
        return result
    
    return wrapper

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))  # Computes
print(fibonacci(10))  # Cache hit

Rate Limiting Decorator

import time

def rate_limit(calls_per_minute):
    interval = 60 / calls_per_minute
    last_called = [0]  # Use list to modify in nested function
    
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            if elapsed < interval:
                wait_time = interval - elapsed
                print(f"Rate limited. Waiting {wait_time:.1f} seconds...")
                time.sleep(wait_time)
            
            last_called[0] = time.time()
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(calls_per_minute=2)
def api_call():
    print("Making API call...")
    return "API response"

for i in range(3):
    api_call()
    time.sleep(0.1)

Common Patterns and Best Practices

1. Always Use functools.wraps

# Good
@functools.wraps(func)
def wrapper(...):
    ...

# Bad - loses original function metadata
def wrapper(...):
    ...

2. Handle All Arguments

# Good - accepts any arguments
def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Bad - only works with specific signatures
def decorator(func):
    def wrapper(a, b):
        return func(a, b)
    return wrapper

3. Document Your Decorators

def my_decorator(param):
    """
    A decorator that does something useful.
    
    Args:
        param: Description of the parameter
        
    Returns:
        Decorated function
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Implementation
            return func(*args, **kwargs)
        return wrapper
    return decorator

Key Points to Remember

  • Decorators are functions that modify other functions' behavior
  • The @ symbol is syntactic sugar for applying decorators
  • Use *args and **kwargs to handle any function signature
  • functools.wraps preserves the original function's metadata
  • Decorators can accept arguments using decorator factories
  • Multiple decorators are applied from bottom to top

Function decorators are powerful, but you can also decorate entire classes. In the next lesson, we'll explore class decorators and how they can modify class behavior before instances are even created.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service