decorators

Published 2025-05-13

A decorator is a function that takes another function as its argument and returns a new function that can incorporate the original function’s behavior and add functionality to it.

Timing decorator example:

def timing_decorator(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = f(*args, **kwargs)
        print(f"Function {f.__name__} took {time.perf_counter() - start} seconds to execute.")
        return result
    return wrapper
@timing_decorator
def quick_function(n):
    """A sample function that does a quick computation."""
    result = sum(i*i for i in range(n))
    return f"Sum of squares up to {n-1} is {result}"

The decorator syntax:

@timing_decorator
def f():
pass

is equivalent to:

f = timing_decorator(f)

timing_decorator returns the wrapper over the inputted function, which is closed over1 1. See the closures snippet. by the wrapper. Positional and keyword arguments to the function are passed to the wrapper, which can return the function’s output, a modified output, or something else altogether (such as wall time).

@functools.wraps(f) copies the metadata of f to the function it’s decorating. It’s used here because the metadata of f, including __name__ and __doc__, gets overridden by wrapper. Adding @functools.wraps(f) to the wrapper function therefore preserves f’s metadata.

print(f"Name of quick_function: {quick_function.__name__}")
# Output: Name of quick_function: quick_function
print(f"Docstring of quick_function: {quick_function.__doc__}")
# Output: Docstring of quick_function: A sample function that does a quick computation.
  1. See the closures snippet for a refresher on how inner functions hang on to outer-scope state.