Python装饰器注入参数的惯用方法

5

所以,这是一个由两部分组成的问题 -

  1. 在使用装饰器时,Python中是否有惯用的方式将参数注入函数签名中?

例如:

def _mydecorator(func):
  def wrapped(someval, *args, **kwargs):
    do_something(someval)
    return func(*args, **kwargs)
  return wrapped

@_mydecorator
def foo(thisval, thatval=None):
  do_stuff()

这其中的原因在于使用SaltStack的运行模块时,您可以在模块中定义函数,并且可以通过“salt-run”命令调用这些函数。如果上述示例是一个名为“bar”的Salt运行模块,那么我可以运行:

salt-run bar.foo aval bval

盐源运行通过导入模块并使用您在命令行上给出的参数调用函数。以_开头或位于类中的模块中的任何函数都将被忽略,并且无法通过盐源运行运行。

因此,我希望定义类似于超时修饰符的东西来控制函数运行的时间。

我意识到我可以做类似以下的事情:

@timeout(30)
def foo():
  ...

但我希望将该值设置为可配置的,这样我就可以运行类似以下的代码:

salt-run bar.foo 30 aval bval
salt-run bar.foo 60 aval bval

上述装饰器可以使用,但感觉有些不自然,因为它改变了函数的签名,除非用户查看装饰器,否则他们无法了解。

我还有另外一种情况,我想要创建一个装饰器,在 Salt 运行程序执行之前处理“prechecks”(预检查)。但是,这个 precheck 需要从被装饰的函数中获取一些信息。以下是一个示例:

def _precheck(func):
  def wrapper(*args, **kwargs):
    ok = False
    if len(args) > 0:
      ok = run_prechecks(args[0])
    else:
      ok = run_prechecks(kwargs['record_id'])
    if ok:
      func(*args, **kwargs)
  return wrapper

@_precheck
def foo(record_id, this_val):
  do_stuff()

这种方式似乎有些笨拙,因为它要求被装饰的函数a)有一个名为“record_id”的参数,并且b)它是第一个参数。

现在,因为我正在编写所有这些函数,所以这不算什么大问题,但似乎还有更好的方法来解决这个问题(比如不使用装饰器来尝试解决这个问题)。


1
我不知道你使用的是哪个Python版本(从Salt文档示例中猜测可能是2.x),但如果你可以使用Python 3,那么你的装饰器可以注入一个关键字参数,并设置默认值,你在调用时可以选择性地覆盖它。 - Blckknght
2个回答

0
动态定义装饰器参数的方式不是使用语法糖 (@)。可以像这样实现: func = dec(dec_arguments)(func_name)(func_arguments)
import json
import sys

foo = lambda record_id, thatval=None: do_stuff(record_id, thatval)
def do_stuff(*args, **kwargs):
    # python3
    print(*args, json.dumps(kwargs))

def _mydecorator(timeout):
    print('Timeout: %s' % timeout)
    def decorator(func):
        def wrapped(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapped
    return decorator


if __name__ == '__main__':
    _dec_default = 30
    l_argv = len(sys.argv)
    if  l_argv == 1:
        # no args sent
        sys.exit('Arguments missing')
    elif l_argv == 2:
        # assuming record_id is a required argument
        _dec_arg = _dec_default
        _args = 1
    else:
        # three or more args: filename 1 2 [...]
        # consider using optional keyword argument `timeout`
        # otherwise in combination with another optional argument it's a recipe for disaster
        # if only two arguments will be given - with current logic it will be tested for `timeoutedness`
        try:
            _dec_arg = int(sys.argv[1])
            _args = 2
        except (ValueError, IndexError):
            _dec_arg = _dec_default
            _args = 1
    foo = _mydecorator(_dec_arg)(foo)(*sys.argv[_args:])

0

据我所知,在Python 3.7中没有惯用的方法来实现这一点。事实上,只有在不修改签名时@functools.wraps才起作用(请参见what does functools.wraps do ?)。

然而,有一种方法可以做到与@functools.wraps相同的效果(公开完整的签名,保留__dict__和文档字符串):@makefun.wraps。使用这个可插拔的@wraps替代品,您可以编辑公开的签名

from makefun import wraps

def _mydecorator(func):
  @wraps(func, prepend_args="someval")
  def wrapped(someval, *args, **kwargs):
    print("wrapper executes with %r" % someval)
    return func(*args, **kwargs)
  return wrapped

@_mydecorator
def foo(thisval, thatval=None):
  """A foo function"""
  print("foo executes with thisval=%r thatval=%r" % (thisval, thatval))

# let's check the signature
help(foo)

# let's test it
foo(5, 1)

返回:

Help on function foo in module __main__:

foo(someval, thisval, thatval=None)
    A foo function

wrapper executes with 5
foo executes with thisval=1 thatval=None

你可以看到暴露的签名包含了前置参数。

相同的机制也适用于追加和删除参数,以及更深入地编辑签名。有关详细信息,请参见makefun文档(顺便说一下,我是作者;))


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