Python Decorators: Inspecting Function Arguments

Suppose you want to write a decorator for a function that will execute a callback on a function’s argument. You also want to specify the name of that argument so that it doesn’t matter where it appears in the function’s signature, or how the function was called (you can give arguments to a function in any order if you use keyword arguments).

More concretely, we want to be able to write

@autocall(callback, param_name='x')
def f(w, x, y):

And have callback(x) execute whenever f() is called. To do this, we’ll need to map parameter names to arguments, and then we’ll simply grab the value with name ‘x’. Luckily, Python’s inspect module makes this easy for us.

To determine a function’s signature, we can use inspect.signature(). This returns a Signature object which contains the parameters. We won’t use the Parameter objects themselves, however. Instead, we’ll use the Signature.bind(*args, **kwargs) method which creates a mapping from parameter names to argument values (given some parameters *args, **kwargs). In our decorator we have the arguments given to f(), so we simply execute this within our wrapper function to get the mapping from ‘x’ to its value. In case ‘x’ has a default parameter, and it’s not set in *args or **kwargs, we must also call the BoundArguments.apply_defaults() method.

A full example of all this is the following.

import inspect
from functools import wraps

def autocall(callback, param_name='the_name'):

    def decorator(f):
        sig = inspect.signature(f)

        if param_name not in sig.parameters:
            raise ValueError(
                f"Wrapped function has no parameter '{param_name}'"

        def wrapper(*args, **kwargs):
            bound_arguments = sig.bind(*args, **kwargs)

            value = bound_arguments.arguments[param_name]


            return f(*args, **kwargs)

        return wrapper

    return decorator

def print_callback(val):
    print(f"Value is {val}")

@autocall(print_callback, param_name='x')
def function(w, x, y=3, z=4):
    print(w, x, y, z)

if __name__ == "__main__":
    function(1, 2, 3, 4)
    function(x=2, w=1)
    function(y=3, x=2, w=1)

    # Printed:
    #   Value is 2
    #   1 2 3 4
    #   Value is 2
    #   1 2 3 4
    #   Value is 2
    #   1 2 3 4

In terms of applications, logging is an obvious one (given my print callback), but this same idea can also be used in cases when you want to remove some boilerplate code but allow the wrapped function’s signature to change.