Python默认参数装饰器

7

Python 3.6

我正在尝试创建一个装饰器,自动将参数的字符串分配为默认值。

例如:

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

将等同于:

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

这是我尝试使用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)

静态方法 default_signature 创建函数所需的签名,但我在尝试将新签名分配给函数时遇到了困难。我正在尝试使用 Signature.bind。我已经阅读了文档,但我似乎还是漏掉了什么。 编辑 结合 Ashwini Chaudhary 的回答:
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)

你尝试过替换__defaults__属性吗? - Ignacio Vazquez-Abrams
承认并非如此,我猜我希望避免手动操作,并让Signature类处理它。 - James Schinner
“签名对象是不可变的。” - Ignacio Vazquez-Abrams
2个回答

6

这似乎是有效的:

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

它依赖于Python中可以通过关键字传递位置参数的事实。例如,如果你有一个函数:
def foo(a, b):
    ...

您完全有权称之为:

foo(b=1, a=2)

我的解决方案可以找出您传递了多少个位置参数,并使用它来确定哪些位置参数未被传递。然后,我将这些位置参数名称添加到kwargs字典中。
酷的是,如果有人需要在python2.x中实现此目的,他们只需要将getfullargspec更改为getargspec,就可以正常工作。
关于速度的说明:
与Ashwini的优秀解释相比,我的简单装饰器大约快10倍,而不用操作签名对象。
@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

他的解决方案实际上更新了函数的 __signature__(这真的很棒)。原则上,您可以采用 Signature 创建逻辑,并将其添加到我的解决方案中以更新 __signature__,但保留 argspec 样式逻辑用于实际计算...

@JamesSchinner -- Ashwini的测试用例提醒我存在一个小错误--基本上,我需要确保在kwargs中已经存在默认值时不要再添加它们。 - mgilson
这非常干净。如果我对我的代码没有情感投入,我会使用它。我选择了Ashwini的,因为它解释了我之前理解错误的地方。 - James Schinner
没问题 - 我同意你给他打勾。事实上,他的回答非常好,能很好地回答你的问题。在很多方面,它比我的回答更好,因为它更详细地解释了你所问的 API。我认为这是一个很好的演示,说明为什么 StackOverflow 允许 多个 答案 :-)。 - mgilson
似乎“绿色勾号”只能应用于一个。 - James Schinner

5
绑定参数和关键字后,您需要调用apply_defaults方法,以设置缺少参数的默认值。BoundArguments实例将使用argskwargs属性来调用函数。请注意,保留HTML标记。
def __call__(self, *args, **kwargs):
    ba = self.sig.bind(*args, **kwargs)
    ba.apply_defaults()
    return self.func(*ba.args, **ba.kwargs)

演示:

>>> @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

一个功能版本的代码,同时更新被装饰函数的签名:

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

演示:

>>> 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={}
)

感谢提供函数式版本。太棒了,我会使用它的。 - James Schinner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接