2

For logging purpose I tried to automatically catch the arguments passed to a function inside this function and convert them to a dict of arg : value. I tried the inspect.signature() module but it only provides with the default input of the function and not the argument passed to it

import inspect

def my_add_func(a, b ,c=3):
    sig = inspect.signature(my_add_func)
    print("my_add_func args : {0}".format(sig))
    return a + b + c

if __name__ == '__main__':
    my_add_func(10, 2, 3)

Outputs:

(a, b, c=3)

Whereas I would like to have:

{a: 10, b: 2, c:3}

How can I do that?

MSeifert
  • 145,886
  • 38
  • 333
  • 352
MCMZL
  • 1,078
  • 9
  • 21

2 Answers2

5

You could use signature.bind:

import inspect

def my_add_func(a, b ,c=3):
    sig = inspect.signature(my_add_func)
    print("my_add_func args : {0}".format(sig.bind(a, b, c)))  # changed here
    return a + b + c

if __name__ == '__main__':
    my_add_func(10, 2, 3)

which gives:

my_add_func args : <BoundArguments (a=10, b=2, c=3)>

If you want it as a mapping (dict-like) you can access the BoundArguments.arguments property:

>>> sig.bind(a, b, c).arguments
OrderedDict([('a', 10), ('b', 2), ('c', 3)])

or even convert it to a plain dict:

>>> dict(sig.bind(a, b, c).arguments))
{'a': 10, 'b': 2, 'c': 3}
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 1
    Works as expected thank you, I would just add the `apply_default()` as suggested by @Yuval Ben Ari. Also I found out that `locals()` was easier to use. It fits my case, because I only want to catch the arguments at the very beginning of the function. – MCMZL Aug 06 '17 at 14:02
  • @MCMZL If you do it the check directly in the function you don't need the `apply_default()` that's only interesting in case you try to inspect a function from the outside. – MSeifert Aug 06 '17 at 15:13
  • @MCMZL I'm glad it worked, please don't forget to [accept](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work) the most helpful answer :) – MSeifert Aug 06 '17 at 15:14
1

You should use wrappers if you want to log function inputs:

import inspect

def log_args(func):
    def log_and_call(*args, **kwargs):
        sig = inspect.signature(func)
        bound_args = sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        loggable_args = bound_args.arguments

        print("Function name: " + func.__name__)
        for key, val in loggable_args.items():
            print(key + " = " + str(val))

        return func(*args, **kwargs)
    return log_and_call

@log_args
def do_sum(a, b, c=0):
    return a + b + c


x = do_sum(1,b=2)

This way it is easy to log every function call, and it prevents boilerplate code.
Notice that I used apply_defaults so it will print the value of the default arguments, if not provided with different values.
In the example I also gave the name of b although it is not an argument with a default value, just to show it works.
You can replace the prints with writes to a logger or something similar.

Yuval Ben-Arie
  • 1,280
  • 9
  • 14
  • 1
    @MSeifert yes, I used `apply_defaults`. I explained why in the edit – Yuval Ben-Arie Aug 05 '17 at 11:34
  • @MSeifert I also did not convert to `dict`, so the order of the arguments will not be lost. – Yuval Ben-Arie Aug 05 '17 at 11:36
  • Thank you the `apply_default()` is very useful indeed. I also found `locals()` described [here](https://stackoverflow.com/questions/7969949/whats-the-difference-between-globals-locals-and-vars) that can be used in my case as well – MCMZL Aug 06 '17 at 14:05