Manifestation Techniques by Zodiac · CodeAmber

How to Implement a Custom Decorator in Python

A custom decorator in Python is a higher-order function that takes another function as an argument and extends its behavior without explicitly modifying its source code. It works by wrapping the original function in a closure, allowing the developer to execute code before and after the wrapped function runs.

How to Implement a Custom Decorator in Python

Understanding the Decorator Pattern

In Python, functions are first-class objects, meaning they can be passed as arguments to other functions and returned as values. A decorator leverages this capability to "wrap" a target function. When you apply a decorator using the @decorator_name syntax, Python passes the decorated function into the decorator and replaces the original function with the wrapper returned by the decorator.

This pattern is essential for implementing cross-cutting concerns—functionality that affects multiple parts of an application—such as logging, access control, and caching, without duplicating code across the codebase.

Implementing a Basic Function Decorator

To create a basic decorator, define a function that contains a nested "wrapper" function. The wrapper executes the desired logic and then calls the original function using the *args and **kwargs syntax to ensure it can handle any number of positional and keyword arguments.

Implementation Example

def debug_log(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

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

# Output: 
# Calling add_numbers with args: (5, 10), kwargs: {}
# add_numbers returned: 15

Preserving Metadata with functools.wraps

A common issue with basic decorators is that the decorated function loses its original identity; its __name__ and __doc__ attributes are replaced by those of the wrapper. To prevent this, Python provides the functools.wraps decorator.

Applying @functools.wraps(func) inside the decorator ensures that the original function's metadata is copied to the wrapper. This is a critical best practice for professional software development, as it ensures that debugging tools and documentation generators continue to work correctly.

from functools import wraps

def authorized(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Logic to check authorization
        return func(*args, **kwargs)
    return wrapper

Creating Decorators with Arguments

When a decorator needs to accept its own configuration parameters (e.g., specifying a user role for access control), an additional layer of nesting is required. The outer function accepts the configuration arguments and returns the actual decorator function.

Example: A Repeat Decorator

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}!")

Implementing Class-Based Decorators

While function-based decorators are common, class-based decorators provide a more structured approach for maintaining state. A class-based decorator implements the __call__ magic method, which allows an instance of the class to be called like a function.

Class decorators are particularly useful when the decorator needs to track data across multiple calls to the decorated function, such as a call counter or a sophisticated cache.

Class-Based Implementation

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

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

@CallCounter
def say_hi():
    print("Hi!")

Practical Use Cases in Professional Development

Custom decorators are widely used in industry-standard frameworks to separate business logic from infrastructure concerns. Common implementations include:

For developers seeking to master these implementation patterns, CodeAmber provides comprehensive guides on optimizing Python code and applying clean architecture principles to ensure these decorators remain maintainable as a project scales.

Common Pitfalls and Best Practices

  1. Avoid Over-Decorating: Excessive use of decorators can make the control flow difficult to follow, complicating the debugging process.
  2. Always Use wraps: Never omit functools.wraps in a production environment, as it breaks introspection.
  3. Keep Wrappers Lean: The logic inside the wrapper should be efficient. Since the wrapper executes every time the function is called, any performance lag here will multiply across the application.
  4. Handle Return Values: Ensure the wrapper always returns the result of the decorated function unless the specific intent of the decorator is to suppress the return value.

Key Takeaways

Original resource: Visit the source site