Decorators 101: How Do Python Decorators "Add Functionality" to Functions?

In Python, functions are “first-class citizens”—they can be passed as arguments, assigned to variables, or even returned as values. Decorators leverage this feature to “dynamically add functionality to functions” without modifying the original function’s code. It’s like wrapping a gift box with beautiful paper, preserving the gift itself while adding a new appearance.

1. Why Decorators Are Needed?

Suppose we want to add “print execution logs” functionality to multiple functions. Directly modifying each function would lead to repetitive and hard-to-maintain code. For example:

# Original function 1
def add(a, b):
    return a + b

# Original function 2
def subtract(a, b):
    return a - b

To add logging, the straightforward approach would be to manually modify each function:

# Log-added add function
def add(a, b):
    print(f"Function add started, parameters: a={a}, b={b}")
    result = a + b
    print(f"Function add finished, result: {result}")
    return result

# Log-added subtract function (repetitive code)
def subtract(a, b):
    print(f"Function subtract started, parameters: a={a}, b={b}")
    result = a - b
    print(f"Function subtract finished, result: {result}")
    return result

This is clearly inelegant. If there are 100 functions, you’d repeat the logging code 100 times. Decorators solve this “reinventing the wheel” problem.

2. Basic Principle of Decorators

A decorator is essentially a function that takes a function as input and returns a new function (the “wrapper” that adds extra functionality around the original function).

1. Simplest Decorator

Let’s define a “log decorator” that adds “start/end execution” logs to any function:

def log_decorator(func):  # Accepts the original function `func`
    def wrapper(*args, **kwargs):  # Internal wrapper function
        # 1. Add functionality: print start log
        print(f"Function {func.__name__} started, parameters: {args}, {kwargs}")
        # 2. Call the original function
        result = func(*args, **kwargs)
        # 3. Add functionality: print end log
        print(f"Function {func.__name__} finished, result: {result}")
        return result
    return wrapper  # Return the wrapped function

2. Using the Decorator

Use the @ syntax (decorator sugar) to “wrap” the original function:

@log_decorator  # Equivalent to: add = log_decorator(add)
def add(a, b):
    return a + b

@log_decorator  # Equivalent to: subtract = log_decorator(subtract)
def subtract(a, b):
    return a - b

3. Testing the Call

When you call add(1, 2), the wrapper function executes, automatically printing logs:

add(1, 2)
# Output:
# Function add started, parameters: (1, 2), {}
# Function add finished, result: 3

3. Core Details of Decorators

1. *args and **kwargs

  • *args captures positional arguments (e.g., 1, 2 in add(1, 2)).
  • **kwargs captures keyword arguments (e.g., a=1, b=2 in add(a=1, b=2)).
  • This ensures the decorator works with any function’s parameters.

2. Preserving Original Function Metadata

Without modification, add.__name__ would return wrapper (since add is replaced by wrapper). Use functools.wraps to preserve metadata:

from functools import wraps

def log_decorator(func):
    @wraps(func)  # Preserves original function metadata
    def wrapper(*args, **kwargs):
        print(f"Function {func.__name__} started")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} finished")
        return result
    return wrapper

print(add.__name__)  # Output: add (not "wrapper")

4. Decorators with Parameters

To pass parameters to a decorator (e.g., custom log prefixes), nest two layers of functions:

def log_decorator(prefix="【Log】"):  # Outer function: accepts decorator arguments
    def decorator(func):  # Inner function: accepts original function
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{prefix} Function {func.__name__} started")
            result = func(*args, **kwargs)
            print(f"{prefix} Function {func.__name__} finished")
            return result
        return wrapper
    return decorator  # Return the inner decorator

# Usage with parameters
@log_decorator(prefix="【Debug】")
def multiply(a, b):
    return a * b

multiply(3, 4)
# Output:
# 【Debug】 Function multiply started
# 【Debug】 Function multiply finished

5. Use Cases of Decorators

Decorators are flexible for scenarios like:
- Logging: Record function calls, parameters, and return values.
- Performance Testing: Measure execution time (e.g., @timer_decorator).
- Permission Verification: Check user permissions before execution (e.g., @check_permission).
- Caching: Cache results to avoid redundant computations (e.g., @cache_decorator).

6. Execution Order of Multiple Decorators

When multiple decorators wrap a function, they execute from bottom to top (the decorator closer to the function runs first):

@decorator2
@decorator1
def func():
    pass

Execution order: decorator2decorator1funcdecorator1decorator2.

Summary

Decorators are Python’s “magic tools” that use function nesting and closures to add functionality elegantly. Key points:
1. A decorator is a function that takes a function and returns a new function.
2. The @ syntax simplifies applying decorators.
3. *args/**kwargs handle arbitrary parameters, and functools.wraps preserves metadata.
4. Parameterized decorators require two nested functions.

From simple logging to complex permission checks, decorators make code cleaner and more maintainable. Try adding a log decorator to your own function to experience its convenience!

Xiaoye