Skip to main content

Command Palette

Search for a command to run...

Python Decorators: A Comprehensive Guide

Published
•5 min read•View as Markdown

Core Concepts of Decorators

What is a Decorator?

  • A decorator is a function that takes another function as an argument, adds some functionality (via a wrapper), and returns a new function that behaves like the original but with enhancements.

  • Syntax: Use the @decorator_name annotation right above a function definition to apply it. This is syntactic sugar for function = decorator_name(function).

  • Key Components:

    • Outer Function: Accepts the target function (e.g., func).

    • Wrapper Function: Nested inside the outer function; handles the actual logic (e.g., pre/post-execution actions) and calls the original function with args and *kwargs for flexibility.

    • Return: The wrapper is returned, replacing the original function.

  • Why Use Decorators?

    • Reusability: Apply the same enhancement (e.g., login check) to multiple functions.

    • Clean Code: Avoid repetitive boilerplate in function bodies.

    • Common in Frameworks: Like Django's @login_required for route protection.

Basic Structure

Python

def my_decorator(func):
    def wrapper(*args, **kwargs):  # Handles any arguments
        # Pre-execution logic (e.g., print start time)
        result = func(*args, **kwargs)  # Execute original function
        # Post-execution logic (e.g., print end time)
        return result  # Return original result
    return wrapper

@my_decorator
def my_function(x):
    return x * 2

# Usage: my_function(5)  # Enhanced automatically

Handling Arguments

  • Use args for positional arguments and *kwargs for keyword arguments to make decorators universal.

  • Access function metadata: func.__name__ for the name, func.__doc__ for documentation.

Preserving Function Metadata

To keep the original function's name and docstring intact (useful for introspection), use functools.wraps:

Python

from functools import wraps

def my_decorator(func):
    @wraps(func)  # Preserves metadata
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Problem 1: Timing Function Execution

Goal: Create a decorator that measures and prints the time a function takes to execute. This is useful for performance profiling.

Explanation

  • Import time module.

  • In the wrapper: Record start time before execution, end time after, and compute the difference.

  • Print the function name and elapsed time using func.__name__.

Code Example

Python

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} ran in {end - start:.4f} secs")  # Formatted for readability
        return result
    return wrapper

@timer
def example_function(n):
    time.sleep(n)  # Simulate work

# Usage
example_function(2)  # Output: example_function ran in 2.0012 secs

Key Takeaways

  • Pros: Non-intrusive; works on any function.

  • Edge Cases: Handles async functions poorly (use asyncio for those).

  • Extensions: Log to file instead of printing, or return time as a value.

Problem 2: Debugging Function Calls

Goal: Build a decorator that prints the function name and argument values every time the function is called. Ideal for troubleshooting.

Explanation

  • In the wrapper: Convert *args to a comma-separated string using list comprehension and str().

  • For **kwargs: Iterate over items and format as key:value.

  • Print a formatted message before executing the function.

Code Example

Python

def debug(func):
    def wrapper(*args, **kwargs):
        args_value = ', '.join(str(arg) for arg in args)
        kwargs_value = ', '.join(f"{k}: {v}" for k, v in kwargs.items())
        print(f"Calling '{func.__name__}' with args [{args_value}] and kwargs [{kwargs_value}]")
        return func(*args, **kwargs)
    return wrapper

@debug
def hello():
    print("Hello")

@debug
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

# Usage
hello()  # Output: Calling 'hello' with args [] and kwargs []
         #         Hello

greet("Alice", greeting="Hi")  # Output: Calling 'greet' with args [Alice] and kwargs [greeting: Hi]
                               #         Hi, Alice!

Key Takeaways

  • Pros: Great for logging inputs without modifying function code.

  • Edge Cases: Handle empty args/kwargs gracefully (e.g., empty strings).

  • Extensions: Add timestamps or log levels (e.g., integrate with logging module).

Problem 3: Caching Return Values

Goal: Implement a decorator that stores function results in a cache (dictionary). On repeated calls with identical arguments, return the cached value instead of re-executing. Useful for expensive computations (e.g., API calls).

Explanation

  • Use a dictionary (cache_values) to store results, keyed by tuple of *args (tuples are hashable).

  • In the wrapper: Check if args are in cache; if yes, return cached result. Otherwise, execute, cache, and return.

  • Note: Ignores **kwargs for simplicity; extend with args + tuple(sorted(kwargs.items())) for full support.

Code Example

Python

import time

def cache(func):
    cache_values = {}  # Function-local cache
    def wrapper(*args):
        if args in cache_values:
            print(f"Cache hit for {args}")  # Optional debug
            return cache_values[args]
        result = func(*args)
        cache_values[args] = result
        print(f"Cache miss; computed {result} for {args}")  # Optional debug
        return result
    return wrapper

@cache
def long_running_function(a, b):
    time.sleep(3)  # Simulate expensive operation
    return a + b

# Usage
print(long_running_function(2, 3))  # Waits 3s: Cache miss; computed 5 for (2, 3) \n 5
print(long_running_function(2, 3))  # Instant: Cache hit for (2, 3) \n 5
print(long_running_function(4, 1))  # Waits 3s: Cache miss; computed 5 for (4, 1) \n 5

Key Takeaways

  • Pros: Dramatically improves performance for idempotent functions.

  • Edge Cases: Cache grows indefinitely (use LRU cache via functools.lru_cache for limits). Not thread-safe without locks.

  • Extensions: Add expiration (TTL) or persist to file/database.

Comparison of Examples

DecoratorPurposeKey Wrapper LogicUse Case Example
TimerMeasure execution timestart/end = time.time(); print diffProfiling slow functions
DebugLog calls & argsJoin args/kwargs as strings; printDebugging inputs/outputs
CacheMemoize resultsDict check/insert on args; return cachedAvoiding recomputation