The ability to use introspection to dynamically determine the current code context and frame/stack can be a powerful tool for a developer.
Introspection can be applied for debugging, logging, metrics collection, or method overloading based on type.
Building on my earlier article on the basics of introspection, I will now show how to inject custom logic into a function by using the @ “pie” syntax and a custom decorator.
Pie syntax
We want to be able to decorate any method with introspection logic. To accomplish that we will be using the pie syntax (@).
Below is an example of adding our custom decorator showargs_decorator to the simple function named show_math_result.
@showargs_decorator def show_math_result(a,b,showLower=True): opDisplay = "plus" if showLower else "PLUS" print("{} {} {} = {}".format(a, op_display, b, a + b)) return a+b
The idea in this article is to write our showargs_decorator in a generic enough way that we can decorate any function with this logic.
Custom Decorator
Our custom decorator function is shown below. It follows the basic template, returning the callable wrapper at the end.
# custom decorator def showargs_decorator(func): # updates special attributes e.g. __name__,__doc__ @functools.wraps(func) def wrapper(*args, **kwargs): # call custom inspection logic inspect_decorator(func,args,kwargs) # calls original function func(*args, **kwargs) # matches name of inner function return wrapper
It uses the variable positional and keyword arguments (*args,**kwargs) so that it can decorate a function with any number of arguments. We insert ourselves into the function logic by calling out to inspect_decorator, a method we will write and described in the next section.
The functools.wrap updates the double underscore internal variables like function __name__ so it reports properly in the original function.
Custom introspection logic
Our inspect_decorator method uses the original function’s argument specifications to retrieve the descriptions of the arguments that are expected, and marries that with the values passed in *args and **kwargs.
def inspect_decorator(func,args,kwargs): funcname = func.__name__ print("function {}()".format(funcname)) # get description of function params expected argspec = inspect.getargspec(func) # go through each position based argument counter = 0 if argspec.args and type(argspec.args is list): for arg in args: # when you run past the formal positional arguments try: print(str(argspec.args[counter]) + "=" + str(arg)) counter+=1 except IndexError as e: # then fallback to using the positional varargs name if argspec.varargs: varargsname = argspec.varargs print("*" + varargsname + "=" + str(arg)) pass # finally show the named varargs if argspec.keywords: kwargsname = argspec.keywords for k,v in kwargs.items(): print("**" + kwargsname + " " + k + "=" + str(v))
The basic idea is that we get the functions description of its expected parameters using inspect.getargspec, then we map it to the values actually sent to the function (*args and **kwargs).
Sample Program
I have uploaded inspect_func_test_decorator.py to github, which you can use to see the decorator used against functions with varying formal parameters, as well as variable positional and keyword arguments.
REFERENCES
realpython, decorators and usage
python.org, decorators reference
stackoverflow, getting function signature py2 versus py3
thecodeship, writing decorators
artima, more advanced decorator use with decorator args
programiz, explanation of decorator being callable returning callable with syntactic sugar