Python 装饰器向函数及其签名添加参数

Mic*_*ael 2 python python-decorators

我有多个类,其中许多类的一个参数具有相同的初始化代码。因此我想用包装器添加参数。

由于代码已经在生产中,并且此参数在所有调用中都是最后一个,但签名具有不同的长度,并且参数只能是位置,因此从 和 中“捕获”此参数并args非易事kwargs。

只要step是 a ,下面的“工作”就可以kwarg,但如果不是,它就会*args被传递给函数,该函数会正确抛出,因为它有太多参数:

def stepable(func):
    @functools.wraps(func)
    def wrapper(self, *args, step=1, **kwargs):
        func(self, *args, **kwargs)
        self.step = step  # and other stuff, depending on step
    return wrapper
Run Code Online (Sandbox Code Playgroud)

但即使我用(函数参数中len(args)>len(inspect.signature(func).parameters)没有)捕获它,向用户显示的签名也是错误的(因为我使用了)。*args@wraps

如何添加参数(/默认)才能inspect得到它?或者基本上是“做相反的事情functools.partial”?

Ser*_*sta 7

你的问题是functools.wraps复制原始签名。在这里,您必须手动处理和更改它。如果您可以确定包装的方法中没有一个可以具有以下内容,那么就足够简单了:

  • 一个step参数
  • *args(VAR_POSITIONAL)参数
  • **kwargs(VAR_KEYWORD)参数

如果step参数没有默认值

但无论如何,检查模块提供了处理签名的一切。

我将步骤定义为包装函数中的最后一个 POSITIONAL_OR_KEYWORD 参数

可能的代码:

def stepable(func):
    oldsig = inspect.signature(func)
    # search if a VAR_POSITIONAL or VAR_KEYWORD is present
    # if yes insert step parameter before it, else insert it in last position
    params = list(oldsig.parameters.values())
    for i, param in enumerate(params):
        if param.kind == inspect.Parameter.VAR_POSITIONAL:
            break
        if param.kind == inspect.Parameter.VAR_KEYWORD:
            break
    else:
        i = len(params)
    # new parameter name is step or step_[_...] if step if already present
    name = "step"
    while name in oldsig.parameters:
        name += '_'
    newparam = inspect.Parameter(name,
                                 inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                 default = 1)
    params.insert(i, newparam)
    # we can now build the signature for the wrapper function
    sig = oldsig.replace(parameters = params)

    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        bound = sig.bind(self, *args, **kwargs) # compute the bound parameter list
        bound.apply_defaults()
        step = bound.arguments[name]      # extract and remove step
        del bound.arguments[name]
        cr = func(*bound.args, **bound.kwargs) # call original function
        self.step = step
        return cr
    wrapper.__signature__ = sig
    return wrapper
Run Code Online (Sandbox Code Playgroud)

演示:

>>> class A:
    @stepable
    def func(self, a, b=1):
        """This is a test"""
        print(a,b)


>>> a = A()
>>> a.func(5)
5 1
>>> a.step
1
>>> a.func(5,6)
5 6
>>> a.step
1
>>> a.func(5,6,7)
5 6
>>> a.step
7
>>> help(a.func)
Help on method func in module __main__:

func(a, b=1, step=1) method of __main__.A instance
    This is a test
Run Code Online (Sandbox Code Playgroud)