Python > Advanced Python Concepts > Decorators > Decorators with Arguments

Decorators with Arguments: Logging Function Calls

This code snippet demonstrates how to create decorators that accept arguments. We'll build a logging decorator that logs function calls with a customizable log message.

Basic Decorator Structure

This is the fundamental structure of a decorator. It takes a function as input and returns a wrapped function. The wrapper executes code before and/or after the original function.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # Code to execute before calling the function
        result = func(*args, **kwargs)
        # Code to execute after calling the function
        return result
    return wrapper

Decorator with Arguments Structure

To create a decorator that accepts arguments, we introduce an extra layer of function nesting. The outer function (decorator_with_args) accepts the arguments for the decorator. It then returns the actual decorator function (my_decorator), which takes the function to be decorated as input. The inner wrapper function then calls the original function.

def decorator_with_args(arg1, arg2):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            # Code to execute before calling the function, using arg1 and arg2
            result = func(*args, **kwargs)
            # Code to execute after calling the function, using arg1 and arg2
            return result
        return wrapper
    return my_decorator

Example: Logging Decorator with a Message

Here, log_calls takes a message argument. The returned decorator logs a message before and after the function call, including the function's name, arguments, and return value. We use @functools.wraps(func) to preserve the original function's metadata (name, docstring, etc.). This is good practice to avoid unexpected behavior. When calling the decorated functions `add` and `multiply`, the specified log messages ('DEBUG' and 'INFO') are used.

import functools

def log_calls(message):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f'{message}: Calling {func.__name__} with args {args} and kwargs {kwargs}')
            result = func(*args, **kwargs)
            print(f'{message}: {func.__name__} returned {result}')
            return result
        return wrapper
    return decorator

@log_calls('DEBUG')
def add(x, y):
    return x + y

@log_calls('INFO')
def multiply(x, y):
    return x * y

print(add(5, 3))
print(multiply(5, 3))

Output of the Example

The output of the code will be:
DEBUG: Calling add with args (5, 3) and kwargs {}
DEBUG: add returned 8
8
INFO: Calling multiply with args (5, 3) and kwargs {}
INFO: multiply returned 15
15

Real-Life Use Case

Decorators with arguments are widely used in frameworks and libraries for tasks like:

  • Configuration: Setting up resources (e.g., database connections) based on decorator arguments.
  • Authentication and Authorization: Controlling access to functions based on user roles or permissions.
  • Caching: Implementing caching strategies with configurable expiration times or cache keys.
  • Retry Mechanisms: Retrying failed function calls with configurable delays and retry counts.

Best Practices

  • Use @functools.wraps: This preserves the original function's metadata, making debugging and introspection easier.
  • Keep decorators concise: Decorators should ideally perform a specific, well-defined task. Avoid overloading them with too much logic.
  • Handle exceptions carefully: Ensure that exceptions raised within the decorator are handled appropriately to avoid unexpected program termination.
  • Consider using libraries: Libraries like wrapt provide more advanced decorator features and can simplify complex use cases.

Interview Tip

When discussing decorators in an interview, demonstrate a clear understanding of their structure and purpose. Be prepared to explain how they work under the hood and provide real-world examples of their usage. Mentioning @functools.wraps shows that you understand best practices.

When to Use Them

Use decorators when you need to add functionality to multiple functions in a consistent and reusable way. They are particularly useful for cross-cutting concerns like logging, authentication, and caching.

Memory Footprint

Decorators introduce a slight overhead because they create a new function (the wrapper). However, this overhead is typically negligible unless you're decorating a very large number of functions or calling the decorated functions extremely frequently. The memory footprint is primarily determined by the code within the decorator itself.

Alternatives

Alternatives to decorators include:

  • Monkey patching: Modifying the original function directly. This is generally discouraged as it can lead to unexpected behavior and make code harder to maintain.
  • Inheritance: Creating a base class with the desired functionality and having other classes inherit from it. This is suitable when the functionality is specific to a class hierarchy.
  • Manual wrapping: Explicitly calling the additional functionality before and after the function call. This is less concise and reusable than using decorators.

Pros

  • Reusability: Decorators can be applied to multiple functions, reducing code duplication.
  • Readability: They improve code readability by separating concerns (e.g., logging from business logic).
  • Maintainability: Changes to the decorator's functionality automatically apply to all decorated functions.

Cons

  • Complexity: Decorators can be challenging to understand, especially for beginners.
  • Debugging: Debugging decorated functions can be slightly more complex, especially without @functools.wraps.
  • Overhead: They introduce a small performance overhead due to the extra function call.

FAQ

  • Why use @functools.wraps?

    @functools.wraps preserves the original function's metadata (name, docstring, etc.). Without it, the decorated function will appear to have the name and docstring of the wrapper function, making debugging and introspection more difficult. It's best practice to always include it when defining decorators.
  • Can I apply multiple decorators to a single function?

    Yes, you can apply multiple decorators to a single function. The decorators are applied from top to bottom, meaning the decorator closest to the function definition is applied first.
  • How do I access the arguments passed to the decorator within the decorated function?

    The arguments passed to the decorator are available within the decorator's scope. The decorated function receives its usual arguments (*args and **kwargs).