【问题标题】:Python decorator adding argument to function and its signaturePython装饰器向函数及其签名添加参数
【发布时间】:2020-03-11 10:34:05
【问题描述】:

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

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

下面的“有效”,只要stepkwarg,但如果不是,它在*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

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

如何添加参数(/default) 以便inspect 得到它?或者基本上是“做functools.partial的逆操作”?

【问题讨论】:

  • 既然你说是最后一个参数,那func(self, *args[:-1], **kwargs); self.step = args[-1]呢?
  • 感谢您的建议@a_guest!是的,这就是我想说的“但即使我 [w]...”。但是我如何向用户展示这个函数接受step 作为参数呢?
  • 您可以将其添加到__doc__ 字符串中。如果您使用wraps,签名将保持不变。这就是它的工作原理。从您的描述来看,无论如何使用装饰器是不寻常的。创建一个子类并在那里执行初始化似乎更干净。
  • 如果原始方法之一具有 VAR_POSITIONAL (*args) 参数,例如 def func(a, b=1, *args),应该怎么做?您希望包装后的函数签名为def func(a, b=1, step=1, *args) 还是def func(a, b=1, *args)step*args 的最后一个元素,没有默认值?
  • @SergeBallesta 这是一个有效的评论,也是我写“函数[参数]中没有*args”的原因。我的情况不需要它,但def func(a, b=1, step=1, *args) 可能是合适的东西

标签: python python-decorators


【解决方案1】:

您的问题是functools.wraps 复制了原始签名。在这里,您将不得不手动处理和更改它。如果你能确定没有一个被包装的方法可以有的话,那就足够简单了:

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

如果step参数没有默认值

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

我会将 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

演示:

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

【讨论】:

    【解决方案2】:

    希望对你有帮助

    def stepable(func):
        @functools.wraps(func)
        def wrapper(self, *args, step=1, **kwargs):
            return func(self, *args, **kwargs, step=step)
        return wrapper
    
    class A:
        @stepable
        def __init__(self,a, b, **kwargs):
            self.a = a
            self.b = b
            for k in kwargs:
                setattr(self, k, kwargs[k])
    

    它将提供的步骤或 1 传递给函数 尽管您必须仅在实例化时将 step 作为命名参数传递,但它会使您的代码更具可读性。

    【讨论】:

      猜你喜欢
      • 2010-12-23
      • 2021-03-28
      • 2013-09-14
      • 1970-01-01
      • 2020-02-06
      • 2014-07-21
      • 1970-01-01
      • 2015-10-19
      • 2017-07-12
      相关资源
      最近更新 更多