Python Decorators and 'functools.wraps'

created:

updated:

tags: python

Decorators

Decorators are Python’s special syntax to run additional code before and after each function call it wraps.

  • Decorators have access to the function’s input arguments, return values and raised exceptions
  • Useful for enforcing semantics, debugging, registering functions, etc.

Example of a Decorator for Debugging

This example is from Effective Python book and is for debugging the stack of nested function calls from a recursive function.

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"{func.__name__}({args!r}, {kwargs!r}) "
              f"-> {result!r}")
        return result
    return wrapper

@trace
def fibonacci(n):
    """Return the n-th Fibonacci number"""
    if n in (0, 1):
        return n
    return (fibonacci(n - 2) + fibonacci(n - 1))

@trace is equivalent to wrapping fibonacci function as below: fibonacci = trace(fibonacci)

The decorated function runs the wrapper code before and after fibonacci runs. Therefore, it prints arguments and return values at each level in the recursive stack.

Side Effects of Decorators

The value returned by the decorator points at trace.<locals>.wrapper instead of the fibonacci function. This can be problematic when we use debugger to introspect the code.

functools.wraps

The solution toe the above side effect is to use functools built-in module. functools has a decorator that helps write decorators by copying all of the important metadata about the inner function to the outer function.

from functools import wraps

def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ...
    return wrapper

@trace
def fibonacci(n):
    ...

Now, when we take a look at help(fibonacci), it returns description of fibonacci function instead of wrapper.

Reference

  • Effective Python