python decorator to display passed AND default kwa

2019-03-30 09:53发布

问题:

I am new to python and decorators and am stumped in writing a decorator which reports not only passed args and kwargs but ALSO the unchanged default kwargs.

This is what I have so far.

def document_call(fn):
    def wrapper(*args, **kwargs):
        print 'function %s called with positional args %s and keyword args %s' % (fn.__name__, args, kwargs)
        return fn(*args, **kwargs)
    return wrapper

@document_call
def square(n, trial=True, output=False):
    # kwargs are a bit of nonsense to test function
    if not output:
        print 'no output'
    if trial:
        print n*n

square(6) # with this call syntax, the default kwargs are not reported
# function square called with positional args (6,) and keyword args {}
# no output
36

square(7,output=True) # only if a kwarg is changed from default is it reported
# function square called with positional args (7,) and keyword args {'output': True}
49

The 'problem' is that this decorator reports the args that are passed in the call to square but does not report the default kwargs defined in the square definition. The only way kwargs are reported is if they're changed from their default i.e. passed to the square call.

Any recommendations for how I get the kwargs in the square definition reported too?

Edit after following up on the inspect suggestions, which helped me to the solution below. I changed the output of positional params to include their names because I thought it made the output easier to understand.

import inspect
def document_call(fn):
    def wrapper(*args, **kwargs):            
            argspec = inspect.getargspec(fn)
            n_postnl_args = len(argspec.args) - len(argspec.defaults)
        # get kwargs passed positionally
        passed = {k:v for k,v in zip(argspec.args[n_postnl_args:], args[n_postnl_args:])}
        # update with kwargs
        passed.update({k:v for k,v in kwargs.iteritems()})            
        print 'function %s called with \n  positional args %s\n  passed kwargs %s\n  default kwargs %s' % (
                fn.__name__, {k:v for k,v in zip(argspec.args, args[:n_postnl_args])},
                passed,
                {k:v for k,v in zip(argspec.args[n_postnl_args:], argspec.defaults) if k not in passed})        
        return fn(*args, **kwargs)
return wrapper

That was a good learning experience. It's neat to see three different solutions to the same problem. Thanks to the Answerers!

回答1:

You'll have to introspect the function that you wrapped, to read the defaults. You can do this with the inspect.getargspec() function.

The function returns a tuple with, among others, a sequence of all argument names, and a sequence of default values. The last of the argument names pair up with the defaults to form name-default pairs; you can use this to create a dictionary and extract unused defaults from there:

import inspect

argspec = inspect.getargspec(fn)
positional_count = len(argspec.args) - len(argspec.defaults)
defaults = dict(zip(argspec.args[positional_count:], argspec.defaults))

You'll need to take into account that positional arguments can specify default arguments too, so the dance to figure out keyword arguments is a little more involved but looks like this:

def document_call(fn):
    argspec = inspect.getargspec(fn)
    positional_count = len(argspec.args) - len(argspec.defaults)
    defaults = dict(zip(argspec.args[positional_count:], argspec.defaults))
    def wrapper(*args, **kwargs):
        used_kwargs = kwargs.copy()
        used_kwargs.update(zip(argspec.args[positional_count:], args[positional_count:]))
        print 'function %s called with positional args %s and keyword args %s' % (
            fn.__name__, args[:positional_count], 
            {k: used_kwargs.get(k, d) for k, d in defaults.items()})
        return fn(*args, **kwargs)
    return wrapper

This determines what keyword paramaters were actually used from both the positional arguments passed in, and the keyword arguments, then pulls out default values for those not used.

Demo:

>>> square(39)
function square called with positional args (39,) and keyword args {'trial': True, 'output': False}
no output
1521
>>> square(39, False)
function square called with positional args (39,) and keyword args {'trial': False, 'output': False}
no output
>>> square(39, False, True)
function square called with positional args (39,) and keyword args {'trial': False, 'output': True}
>>> square(39, False, output=True)
function square called with positional args (39,) and keyword args {'trial': False, 'output': True}


回答2:

Since the decorator function wrapper takes any argument and just passes everything on, of course it does not know anything about the parameters of the wrapped function and its default values.

So without actually looking at the decorated function, you will not get this information. Fortunately, you can use the inspect module to figure out the default arguments of the wrapped function.

You can use the inspect.getargspec function to get the information about the default argument values in the function signature. You just need to match them up properly with the parameter names:

def document_call(fn):
    argspec = inspect.getargspec(fn)
    defaultArguments = list(reversed(zip(reversed(argspec.args), reversed(argspec.defaults))))

    def wrapper(*args, **kwargs):
        all_kwargs = kwargs.copy()
        for arg, value in defaultArguments:
            if arg not in kwargs:
                all_kwargs[arg] = value

        print 'function %s called with positional args %s and keyword args %s' % (fn.__name__, args, all_kwargs)

        # still make the call using kwargs, to let the function handle its default values
        return fn(*args, **kwargs)
    return wrapper

Note that you could still improve this as right now you are handling positional and named arguments separately. For example, in your square function, you could also set trial by passing it as a positional argument after n. This will make it not appear in the kwargs. So you’d have to match the positional arguments with your kwargs to get the full information. You can get all the information about the positions from the argspec.



回答3:

Here is the code modified to work with python3

import inspect
import decorator

@decorator.decorator
def log_call(fn,*args, **kwargs):
    sign = inspect.signature(fn)
    arg_names = list(sign.parameters.keys())
    passed = {k:v for k,v in zip(arg_names[:len(args)], args)}
    passed.update({k:v for k,v in kwargs.items()})
    params_str = ", ".join([f"{k}={passed.get(k, '??')}" for k in arg_names])
    print (f"{fn.__name__}({params_str})")
    return fn(*args, **kwargs)

Note I'm using additional library "decorator" as it preserves the function signature.