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}'"
)
@wraps(f)
def wrapper(*args, **kwargs):
bound_arguments = sig.bind(*args, **kwargs)
bound_arguments.apply_defaults()
value = bound_arguments.arguments[param_name]
callback(value)
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.