python decorator to display passed AND default kwargs

狂风中的少年 提交于 2019-12-03 15:35:40

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}

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.

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.

In python 3.6 I did it using inspect.getfullargspec:

def document_call(func):
    @wraps(func)
    def decorator(*args, **kwargs):
        fullargspec = getfullargspec(func)
        default_kwargs = fullargspec.kwonlydefaults
        print('Default kwargs', default_kwargs)
        print('Passed kwargs', kwargs)
        return func(*args, **kwargs)
    return decorator

Be aware about using the * separator when defining the decorated function for this to work

@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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!