Search code examples
pythonfunctionvariablesnames

How to get variable names of function call


I am going to to write a decorator which evaluates the actual names (not their value) of the variables that are passed to the function call.

Below, you find a skeleton of the code which makes it a bit clearer what I want to do.

import functools

def check_func(func):
    # how to get variable names of function call
    # s.t. a call like func(arg1, arg2, arg3)
    # returns a dictionary {'a':'arg1', 'b':'arg2', 'c':'arg3'} ?
    pass

def my_decorator(func):
    @functools.wraps(func)
    def call_func(*args, **kwargs):
        check_func(func)
        return func(*args, **kwargs)

    return call_func

@my_decorator
def my_function(a, b, c):
    pass

arg1='foo'
arg2=1
arg3=[1,2,3]
my_function(arg1,arg2,arg3)

Solution

  • You can't really have what you are asking for.

    There are many ways of calling a function, where you won't even get variable names for individual values. For example, what would the names when you use literal values in the call, so:

    my_function('foo', 10 - 9, [1] + [2, 3])
    

    or when you use a list with values for argument expansion with *:

    args = ['foo', 1, [1, 2, 3]]
    my_function(*args)
    

    Or when you use a functools.partial() object to bind some argument values to a callable object:

    from functools import partial
    func_partial = partial(my_function, arg1, arg2)
    func_partial(arg3)
    

    Functions are passed objects (values), not variables. Expressions consisting of just names may have been used to produce the objects, but those objects are independent of the variables.

    Python objects can have many different references, so just because the call used arg1, doesn't mean that there won't be other references to the object elsewhere that would be more interesting to your code.

    You could try to analyse the code that called the function (the inspect module can give you access to the call stack), but then that presumes that the source code is available. The calling code could be using a C extension, or interpreter only has access to .pyc bytecode files, not the original source code. You still would have to trace back and analyse the call expression (not always that straightforward, functions are objects too and can be stored in containers and retrieved later to be called dynamically) and from there find the variables involved if there are any at all.

    For the trivial case, where only direct positional argument names were used for the call and the whole call was limited to a single line of source code, you could use a combination of inspect.stack() and the ast module to parse the source into something useful enough to analyse:

    import inspect, ast
    
    class CallArgumentNameFinder(ast.NodeVisitor):
        def __init__(self, functionname):
            self.name = functionname
            self.params = []
            self.kwargs = {}
    
        def visit_Call(self, node):
            if not isinstance(node.func, ast.Name):
                return  # not a name(...) call
            if node.func.id != self.name:
                return  # different name being called
            self.params = [n.id for n in node.args if isinstance(n, ast.Name)]
            self.kwargs = {
                kw.arg: kw.value.id for kw in node.keywords
                if isinstance(kw.value, ast.Name)
            }
    
    def check_func(func):
        caller = inspect.stack()[2]  # caller of our caller
        try:
            tree = ast.parse(caller.code_context[0])
        except SyntaxError:
            # not a complete Python statement
            return None
        visitor = CallArgumentNameFinder(func.__name__)
        visitor.visit(tree)
        return inspect.signature(func).bind_partial(
            *visitor.params, **visitor.kwargs)
    

    Again, for emphasis: this only works with the most basic of calls, where the call consists of a single line only, and the called name matches the function name. It can be expanded upon but this takes a lot of work.

    For your specific example, this produces <BoundArguments (a='arg1', b='arg2', c='arg3')>, so an inspect.BoundArguments instance. Use .arguments to get an OrderedDict mapping with the name-value pairs, or dict(....arguments) to turn that into a regular dictionary.

    You'll have to think about your specific problem differently instead. Decorators are not meant to be acting upon the code calling, they act upon the decorated object. There are many other powerful features in the language that can help you deal with the calling context, decorators are not it.