简体   繁体   中英

Default Argument decorator python

Python 3.6

I'm attempting to create a decorator that automatically assigns the string of the argument as the default value.

such as:

def example(one='one', two='two', three='three'):
    pass

would be equivalent to:

@DefaultArguments
def example(one, two, three):
    pass

Here is my attempt (doesn't work.. yet..) DefaultArguments :

from inspect import Parameter, Signature, signature


class DefaultArguments(object):

    @staticmethod
    def default_signature(signature):
        def default(param):
            if param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY):
                return param.replace(default=param.name)
            else:
                return param
        return Signature([default(param) for param in signature.parameters.values()])

    def __init__(self, func):
        self.func = func
        self.sig = self.default_signature(signature(func))

    def __call__(self, *args, **kwargs):
        arguments = self.sig.bind(*args, **kwargs)
        return self.func(arguments)

The staticmethod default_signature creates the desired signature for the function, but I'm having difficulty assigning the new signature to the function. I'm trying to use Signature. bind I've read the docs but i'm missing something.

EDIT

Incorporating Ashwini Chaudhary's answer:

from inspect import Parameter, Signature, signature

class DefaultArguments(object):

    @staticmethod
    def default_signature(signature):
        def default(param):
            if param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY):
                return param.replace(default=param.name)
            else:
                return param
        return Signature([default(param) for param in signature.parameters.values()])

    def __init__(self, func):
        self.func = func
        self.sig = self.default_signature(signature(func))
        print(self.sig)

    def __call__(self, *args, **kwargs):
        ba = self.sig.bind(*args, **kwargs)
        ba.apply_defaults()
        return self.func(*ba.args, **ba.kwargs)

This seems to work:

import inspect

def default_args(func):
    argspec = inspect.getfullargspec(func)

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        unpassed_positional_args = argspec.args[len(args):]
        kwargs.update((a, a) for a in unpassed_positional_args if a not in kwargs)
        return func(*args, **kwargs)

    return wrapper

It relies on the fact that you can pass positional arguments by keyword in python. eg if you have a function:

def foo(a, b):
    ...

You're completely in your rights to call it as:

foo(b=1, a=2)

My solution figures out how many positional arguments you've passed and uses that to figure out which positional arguments weren't passed. I then add those positional argument names to the kwargs dict instead.

And the cool thing here is that if someone needs this for python2.x, they only need to change getfullargspec to getargspec and it should work OK.


A note on speed:

Comparing my solution with Ashwini's excellent explanation shows that the simple decorator is approximately 10x faster than messing around with Signature objects:

@default_args
def foo(a, b, c):
    pass

@DefaultArguments
def bar(a, b, c):
    pass

@default_arguments
def qux(a, b, c):
    pass

import timeit
print(timeit.timeit('foo()', 'from __main__ import foo'))  # 1.72s
print(timeit.timeit('bar()', 'from __main__ import bar'))  # 17.4s
print(timeit.timeit('qux()', 'from __main__ import qux'))  # 17.6

His solution actually updates the __signature__ of the function (which is really nice). In principle, you could take the Signature creation logic and add that to my solution to update the __signature__ but keep the argspec style logic for the actual computation...

After binding the args and keywords with the signature you need to call apply_defaults on the BoundArguments instance to set the default values for missing arguments.

Also the function call will be invoked using BoundArguments 's args and kwargs properties.

def __call__(self, *args, **kwargs):
    ba = self.sig.bind(*args, **kwargs)
    ba.apply_defaults()
    return self.func(*ba.args, **ba.kwargs)

Demo:

>>> @DefaultArguments
... def example(one, two, three):
...         print(one, two, three)
...

>>> example()
one two three
>>> example('spam')
spam two three
>>> example(one='spam', three='eggs')
spam two eggs

A functional version of your code that also updates the signature of the decorated function:

from functools import wraps
from inspect import Parameter, Signature, signature


def default_arguments(func):

    def default(param):
        if param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY):
            param = param.replace(default=param.name)
        return param

    sig = Signature([default(param) for param in signature(func).parameters.values()])

    @wraps(func)
    def wrapper(*args, **kwargs):
        ba = sig.bind(*args, **kwargs)
        ba.apply_defaults()
        return func(*ba.args, **ba.kwargs)

    wrapper.__signature__ = sig
    return wrapper

Demo:

>>> from inspect import getfullargspec    
>>> @default_arguments
... def example(one, two, three):
...         print(one, two, three)
...

>>> getfullargspec(example)
FullArgSpec(
    args=['one', 'two', 'three'],
    varargs=None,
    varkw=None,
    defaults=('one', 'two', 'three'),
    kwonlyargs=[], kwonlydefaults=None, annotations={}
)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM