Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
like image 311
James Schinner Avatar asked Aug 25 '17 02:08

James Schinner


1 Answers

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. e.g. 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...

like image 151
mgilson Avatar answered Sep 22 '22 21:09

mgilson