Print/log the names and values of all parameters a function was called with (without eval)

Question:

When a function is called, I would like to print (or log) the names and values of the parameters it was called with. Something like:

>>> def foo(bar, baz):
>>>    do_something() 
>>> foo(5, 'test')
# would print: foo(bar=5, baz='test')

I can manually add print/logging calls to each function, but this is a pain as the functions I am concerned with grow/change over time and are part of an abstract interface that can be extended in other places.

This answer mentions how to get the parameter names, but doesn’t mention how to get their values when called. I also found this answer which uses eval and works on a known set of parameters. I’d really like to avoid eval and be able to do it for an arbitrary set of parameters.

For context, this code is used for debugging a hardware interface. When the hardware isn’t present it prints (or logs) the IO requests instead of sending them to the hardware.

Asked By: Viglione

||

Answers:

You could try to use func.__code__.co_varnames like this:

def foo(bar, baz):
    magic_parameter_printing() 

bar=0
baz=None  
for var in foo.__code__.co_varnames:
    print(var,'= ',eval(var))

Output:

bar=0
baz=None 
Answered By: MrNobody33

After you’ve add explanation how you want to apply this, I think best way will be to use decorator. It’s more universal solution, because you can add it to any function in your code and it will print all debug info if debug mode is on.

Code:

from functools import wraps

DEBUG = True

def debug_log(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        if DEBUG:
            print(">> Called", function.__name__, "n",
                {**dict(zip(function.__code__.co_varnames, args)), **kwargs})
        result = function(*args, **kwargs)
        if DEBUG:
            print(">>", function.__name__, "return:n", result)
        return result
    return wrapper

@debug_log
def first_example(a, b, c):
    return 100

@debug_log
def second_example(d, e, f):
    return 200

first_example(10, 11, 12)
first_example(c=12, a=10, b=11)
second_example(13, 14, 15)
second_example(e=14, d=13, f=15)
DEBUG = False
first_example(0, 0, 0)
second_example(1, 1, 1)

Output:

>> Called first_example 
 {'a': 10, 'b': 11, 'c': 12}
>> first_example return:
 100
>> Called first_example 
 {'c': 12, 'a': 10, 'b': 11}
>> first_example return:
 100
>> Called second_example 
 {'d': 13, 'e': 14, 'f': 15}
>> second_example return:
 200
>> Called second_example 
 {'e': 14, 'd': 13, 'f': 15}
>> second_example return:
 200
Answered By: Olvin Roght

We have a somewhat flawed answer, but it is different than some of the other answers I have seen. Hopefully, someone can post an even better answer, because my solution to the problem has some serious issues.

import functools
import inspect

def print_calling_args(f):
    assert(callable(f))
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        siggy = inspect.signature(f)
        bound_siggy = siggy.bind(*args, **kwargs)
        all_sargs = ", ".join(kw + " = " + str(arg) for kw, arg in bound_siggy.arguments.items())
        print(f.__qualname__, "(", all_sargs, ")", sep = "")
        return f(*args, **kwargs)
    return wrapper

Issue: str(arg) is bad way to convert arguments into strings

I converted each argument arg into a string by writing str(arg)

Suppose that you pass a pandas dataframe into a function.

We can get some very ugly output.

The resulting string can contain new-line characters n or carriage returns r.

The strings could also be a mile long.

Issue: Cannot Get the Name of a Functor

Another issue with my attempts is that Functors (also known as "function objects") do not have an attribute named __qualname__. That is, if you define a class which has a __call__ method, then we cannot easily print the name of the functor to a string.

funky = Functor()
funky = decorate(funky)
r = funky(1, 2, 3, kwargy = int("python", 36))
# should print:
#     funky(1, 2, 3 kwargy = 1570137287)

Below is an example of a functor:

# EXAMPLE OF A `FUNCTOR`
class Multiplier:   
    def __init__(self, m:int):
        self._m = int(str(m))
    def __call__(self, num:float)
        return num * self._m;

multi_the_multiplier = Multiplier(10**6)

# Decorate `multi_the_multiplier` with ` print_calling_args`
multi_the_multiplier = print_calling_args(multi_the_multiplier)

# ERROR: `multi_the_multiplier` does not have a `__qualname__`

r = multi_the_multiplier(1)
r = multi_the_multiplier(2)
r = multi_the_multiplier(3)
Answered By: Samuel Muldoon