如何创建一个可以使用或不使用参数的 Python 装饰器?

Posted

技术标签:

【中文标题】如何创建一个可以使用或不使用参数的 Python 装饰器?【英文标题】:How to create a decorator that can be used either with or without parameters? 【发布时间】:2010-10-13 19:03:05 【问题描述】:

我想创建一个可以与参数一起使用的 Python 装饰器:

@redirect_output("somewhere.log")
def foo():
    ....

或没有它们(例如默认将输出重定向到标准错误):

@redirect_output
def foo():
    ....

这可能吗?

请注意,我不是在寻找重定向输出问题的不同解决方案,它只是我想要实现的语法示例。

【问题讨论】:

默认的@redirect_output 非常缺乏信息。我建议这是一个坏主意。使用第一种形式,大大简化您的生活。 虽然是一个有趣的问题 - 在我看到它并查看文档之前,我一直认为 @f 与 @f() 相同,但老实说,我仍然认为它应该是(任何提供的参数都会被附加到函数参数上) 这个decorator-factory/decorator模式很好,第一个默认参数function=None,我会走得更远,然后让剩下的参数只用关键字。 【参考方案1】:

这对我有用:

def redirect_output(func=None, /, *, output_log='./output.log'):
    def out_wrapper(func):
        def wrapper(*args, **kwargs):
            res = func(*args, **kwargs)
            print(f"func.__name__ finished, output_log:output_log")
            return res

        return wrapper

    if func is None:
        return out_wrapper  # @redirect_output()
    return out_wrapper(func)  # @redirect_output


@redirect_output
def test1():
    print("running test 1")


@redirect_output(output_log="new.log")
def test2():
    print("running test 2")

test1()
print('-----')
test2()

【讨论】:

【参考方案2】:

完成其他答案:

“有没有办法构建一个可以带参数和不带参数的装饰器?”

没有通用的方法,因为目前在 python 语言中缺少一些东西来检测两个不同的用例。

但是正如bj0s等其他答案已经指出的那样,有一个笨拙的解决方法是检查第一个位置参数的类型和值收到(并检查是否没有其他参数具有非默认值)。如果您保证用户将从不将可调用对象作为装饰器的第一个参数传递,那么您可以使用此解决方法。请注意,这对于类装饰器是相同的(替换上一句中的按类调用)。

为了确定上述内容,我做了相当多的研究,甚至实现了一个名为 decopatch 的库,它结合了上面引用的所有策略(以及更多策略,包括内省)来执行“无论是最智能的解决方法”,具体取决于您的需要。它捆绑了两种模式:嵌套模式和平面模式。

在“嵌套模式”中,你总是返回一个函数

from decopatch import function_decorator

@function_decorator
def add_tag(tag='hi!'):
    """
    Example decorator to add a 'tag' attribute to a function. 
    :param tag: the 'tag' value to set on the decorated function (default 'hi!).
    """
    def _apply_decorator(f):
        """
        This is the method that will be called when `@add_tag` is used on a 
        function `f`. It should return a replacement for `f`.
        """
        setattr(f, 'tag', tag)
        return f
    return _apply_decorator

在“平面模式”下,您的方法直接是应用装饰器时将执行的代码。它被注入装饰函数对象f:

from decopatch import function_decorator, DECORATED

@function_decorator
def add_tag(tag='hi!', f=DECORATED):
    """
    Example decorator to add a 'tag' attribute to a function.
    :param tag: the 'tag' value to set on the decorated function (default 'hi!).
    """
    setattr(f, 'tag', tag)
    return f

但坦率地说,最好的办法是在这里不需要任何库,直接从 python 语言中获取该功能。如果像我一样,您认为遗憾的是,python 语言在今天还不能为这个问题提供一个简洁的答案,不要犹豫,在 python bugtracker 中支持这个想法: https://bugs.python.org/issue36553!

非常感谢您帮助使 python 成为更好的语言 :)

【讨论】:

"than the above" 在***中不是一个有用的短语。随着时间的推移,不同的人会以不同的顺序看到答案。不可能知道你指的是什么答案。 感谢您发现这个@BryanOakley。确实,如果您发现它有用并投票赞成,那么上面的消息就会更少。我相应地编辑了消息【参考方案3】:

由于没有人提到这一点,还有一个使用可调用类的解决方案,我觉得它更优雅,特别是在装饰器很复杂并且可能希望将其拆分为多个方法(函数)的情况下。该解决方案利用__new__ 魔术方法基本上完成了其他人指出的事情。首先检测装饰器的使用情况,然后适当调整返回。

class decorator_with_arguments(object):

    def __new__(cls, decorated_function=None, **kwargs):

        self = super().__new__(cls)
        self._init(**kwargs)

        if not decorated_function:
            return self
        else:
            return self.__call__(decorated_function)

    def _init(self, arg1="default", arg2="default", arg3="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, decorated_function):

        def wrapped_f(*args):
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            print("decorated_function arguments:", *args)
            decorated_function(*args)

        return wrapped_f

@decorator_with_arguments(arg1=5)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments()
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

如果装饰器与参数一起使用,则等于:

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

可以看到参数arg1正确传递给构造函数,装饰函数传递给__call__

但如果装饰器不带参数使用,则等于:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

您会看到,在这种情况下,装饰函数直接传递给构造函数,并且完全省略了对 __call__ 的调用。这就是为什么我们需要在__new__魔术方法中使用逻辑来处理这种情况。

为什么我们不能使用__init__ 而不是__new__?原因很简单:python禁止从__init__返回None以外的任何其他值

警告

这种方法有一个副作用。它不会保留函数签名!

【讨论】:

【参考方案4】:

这可以毫不费力地完成工作:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco

【讨论】:

【参考方案5】:

我知道这是一个老问题,但我真的不喜欢提出的任何技术,所以我想添加另一种方法。我看到 django 在他们的login_required decorator in django.contrib.auth.decorators 中使用了一种非常干净的方法。正如您在decorator's docs 中看到的,它可以单独用作@login_required 或与参数一起使用@login_required(redirect_field_name='my_redirect_field')

他们的做法很简单。他们在装饰器参数之前添加了kwarg (function=None)。如果单独使用装饰器,function 将是它正在装饰的实际函数,而如果使用参数调用它,function 将是None

示例:

from functools import wraps

def custom_decorator(function=None, some_arg=None, some_other_arg=None):
    def actual_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Do stuff with args here...
            if some_arg:
                print(some_arg)
            if some_other_arg:
                print(some_other_arg)
            return f(*args, **kwargs)
        return wrapper
    if function:
        return actual_decorator(function)
    return actual_decorator

@custom_decorator
def test1():
    print('test1')

>>> test1()
test1

@custom_decorator(some_arg='hello')
def test2():
    print('test2')

>>> test2()
hello
test2

@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
    print('test3')

>>> test3()
hello
world
test3

我发现 django 使用的这种方法比这里提出的任何其他技术都更优雅、更容易理解。

【讨论】:

是的,我喜欢这种方法。请注意,您在调用装饰器时必须使用 kwargs,否则第一个位置 arg 被分配给 function,然后事情就中断了,因为装饰器试图调用第一个位置 arg,就好像它是你装饰的一样功能。 是的,第一个参数不是kwarg,它是一个带有默认值的位置参数。但是您可以将其余参数设为仅关键字。【参考方案6】:

这里的几个答案已经很好地解决了您的问题。然而,就风格而言,我更喜欢使用 functools.partial 来解决这个装饰器困境,正如 David Beazley 的 Python Cookbook 3 中所建议的那样:

from functools import partial, wraps

def decorator(func=None, foo='spam'):
    if func is None:
         return partial(decorator, foo=foo)

    @wraps(func)
    def wrapper(*args, **kwargs):
        # do something with `func` and `foo`, if you're so inclined
        pass

    return wrapper

虽然可以,但你可以这样做

@decorator()
def f(*args, **kwargs):
    pass

没有时髦的解决方法,我觉得它看起来很奇怪,我喜欢选择简单地用 @decorator 装饰。

至于次要任务目标,重定向函数的输出在此Stack Overflow post 中解决。


如果您想深入了解,请查看Python Cookbook 3 中的第 9 章(元编程),可免费通过read online 获取。

Beazley 精彩的 YouTube 视频Python 3 Metaprogramming 中对其中一些材料进行了现场演示(还有更多!)。

编码愉快:)

【讨论】:

我最喜欢这种风格,谢谢分享。对于其他人,我会注意到,如果您尝试在 wrapper 中改变 foo,您可能会得到一个 UnboundLocalError,在这种情况下您应该声明 nonlocal foo(或者可能选择不同的局部变量名称,bar , 并设置bar = foo)。另见:***.com/a/57184656/1588795 请注意,此解决方案假定调用代码始终使用关键字参数。在这种情况下,@decorator('foo') 将不会按预期运行。【参考方案7】:

事实上,@bj0 的解决方案中的警告案例可以很容易地检查:

def meta_wrap(decor):
    @functools.wraps(decor)
    def new_decor(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # this is the double-decorated f. 
            # Its first argument should not be a callable
            doubled_f = decor(args[0])
            @functools.wraps(doubled_f)
            def checked_doubled_f(*f_args, **f_kwargs):
                if callable(f_args[0]):
                    raise ValueError('meta_wrap failure: '
                                'first positional argument cannot be callable.')
                return doubled_f(*f_args, **f_kwargs)
            return checked_doubled_f 
        else:
            # decorator arguments
            return lambda real_f: decor(real_f, *args, **kwargs)

    return new_decor

以下是meta_wrap 的这个故障安全版本的一些测试用例。

    @meta_wrap
    def baddecor(f, caller=lambda x: -1*x):
        @functools.wraps(f)
        def _f(*args, **kwargs):
            return caller(f(args[0]))
        return _f

    @baddecor  # used without arg: no problem
    def f_call1(x):
        return x + 1
    assert f_call1(5) == -6

    @baddecor(lambda x : 2*x) # bad case
    def f_call2(x):
        return x + 1
    f_call2(5)  # raises ValueError

    # explicit keyword: no problem
    @baddecor(caller=lambda x : 100*x)
    def f_call3(x):
        return x + 1
    assert f_call3(5) == 600

【讨论】:

谢谢。这很有帮助!【参考方案8】:

我知道这个问题很老,但有些 cmets 是新的,虽然所有可行的解决方案基本相同,但大多数都不是很干净或易于阅读。

就像 thobe 的回答所说,处理这两种情况的唯一方法是检查这两种情况。最简单的方法是简单地检查是否有一个参数并且它是 callabe (注意:如果你的装饰器只接受 1 个参数并且它恰好是一个可调用对象,则需要额外的检查):

def decorator(*args, **kwargs):
    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
        # called as @decorator
    else:
        # called as @decorator(*args, **kwargs)

在第一种情况下,您执行任何普通装饰器所做的操作,返回传入函数的修改或包装版本。

在第二种情况下,您返回一个“新”装饰器,它以某种方式使用通过 *args、**kwargs 传入的信息。

这很好,但是必须为您制作的每个装饰器都写出来可能会很烦人并且不那么干净。相反,能够自动修改我们的装饰器而不必重新编写它们会很好......但这就是装饰器的用途!

使用以下装饰器装饰器,我们可以装饰我们的装饰器,以便它们可以带或不带参数使用:

def doublewrap(f):
    '''
    a decorator decorator, allowing the decorator to be used as:
    @decorator(with, arguments, and=kwargs)
    or
    @decorator
    '''
    @wraps(f)
    def new_dec(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # actual decorated function
            return f(args[0])
        else:
            # decorator arguments
            return lambda realf: f(realf, *args, **kwargs)

    return new_dec

现在,我们可以使用@doublewrap 来装饰我们的装饰器,它们可以使用和不使用参数,但需要注意一点:

我在上面提到过,但应该在这里重复一遍,这个装饰器中的检查对装饰器可以接收的参数进行了假设(即它不能接收单个可调用的参数)。由于我们现在正在使其适用于任何生成器,因此需要牢记它,或者如果它会发生矛盾则对其进行修改。

下面演示了它的用法:

def test_doublewrap():
    from util import doublewrap
    from functools import wraps    

    @doublewrap
    def mult(f, factor=2):
        '''multiply a function's return value'''
        @wraps(f)
        def wrap(*args, **kwargs):
            return factor*f(*args,**kwargs)
        return wrap

    # try normal
    @mult
    def f(x, y):
        return x + y

    # try args
    @mult(3)
    def f2(x, y):
        return x*y

    # try kwargs
    @mult(factor=5)
    def f3(x, y):
        return x - y

    assert f(2,3) == 10
    assert f2(2,5) == 30
    assert f3(8,1) == 5*7

【讨论】:

如果第一个参数是类就不行。代替callable(args[0]),您可以使用isinstance(args[0], types.FunctionType) 检测类 我提到这是我提供的示例中的假设之一。对于传递类等特殊情况,您需要对其进行修改以适合您的情况。【参考方案9】:

根据你是否给它参数,python 装饰器的调用方式完全不同。装饰实际上只是一个(语法限制的)表达式。

在你的第一个例子中:

@redirect_output("somewhere.log")
def foo():
    ....

函数redirect_output 被调用 给定参数,预计返回一个装饰器 函数,它本身以foo 作为参数调用, 其中(终于!)预计会返回最终的装饰函数。

等效代码如下所示:

def foo():
    ....
d = redirect_output("somewhere.log")
foo = d(foo)

第二个示例的等效代码如下所示:

def foo():
    ....
d = redirect_output
foo = d(foo)

所以你可以做你想做的事,但不是完全无缝的:

import types
def redirect_output(arg):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    if type(arg) is types.FunctionType:
        return decorator(sys.stderr, arg)
    return lambda f: decorator(arg, f)

这应该没问题,除非您希望将函数用作 装饰器的参数,在这种情况下装饰器 会错误地认为它没有参数。也会失败 如果这个装饰被应用到另一个装饰 不返回函数类型。

另一种方法是要求 装饰器函数总是被调用,即使它没有参数。 在这种情况下,您的第二个示例将如下所示:

@redirect_output()
def foo():
    ....

装饰器函数代码如下所示:

def redirect_output(file = sys.stderr):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    return lambda f: decorator(file, f)

【讨论】:

【参考方案10】:

您需要检测这两种情况,例如使用第一个参数的类型,并相应地返回包装器(不带参数时)或装饰器(带参数时)。

from functools import wraps
import inspect

def redirect_output(fn_or_output):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **args):
            # Redirect output
            try:
                return fn(*args, **args)
            finally:
                # Restore output
        return wrapper

    if inspect.isfunction(fn_or_output):
        # Called with no parameter
        return decorator(fn_or_output)
    else:
        # Called with a parameter
        return decorator

当使用@redirect_output("output.log") 语法时,redirect_output 使用单个参数"output.log" 调用,并且它必须返回一个接受要作为参数装饰的函数的装饰器。用作@redirect_output时,直接以要修饰的函数作为参数调用。

或者换句话说:@ 语法必须后跟一个表达式,其结果是一个函数,该函数接受要修饰的函数作为其唯一参数,并返回修饰函数。表达式本身可以是函数调用,@redirect_output("output.log") 就是这种情况。令人费解,但真实:-)

【讨论】:

【参考方案11】:

使用带有默认值的关键字参数(如 kquinn 建议的那样)是一个好主意,但需要您包含括号:

@redirect_output()
def foo():
    ...

如果您想要一个在装饰器上不带括号的版本,您必须在装饰器代码中考虑这两种情况。

如果您使用的是 Python 3.0,则可以为此使用仅关键字参数:

def redirect_output(fn=None,*,destination=None):
  destination = sys.stderr if destination is None else destination
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn is None:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator
  else:
    return functools.update_wrapper(wrapper, fn)

在 Python 2.x 中,这可以用可变参数技巧来模拟:

def redirected_output(*fn,**options):
  destination = options.pop('destination', sys.stderr)
  if options:
    raise TypeError("unsupported keyword arguments: %s" % 
                    ",".join(options.keys()))
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn:
    return functools.update_wrapper(wrapper, fn[0])
  else:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator

这些版本中的任何一个都允许您编写如下代码:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...

【讨论】:

你在your code here 里放了什么?你如何调用被装饰的函数? fn(*args, **kwargs) 不起作用。 我认为有一个更简单的答案,创建一个带有可选参数的装饰器的类。创建另一个具有相同参数和默认值的函数,并返回装饰器类的新实例。应该看起来像:def f(a = 5): return MyDecorator( a = a) class MyDecorator( object ): def __init__( self, a = 5 ): .... 抱歉,很难在评论中写出来,但我希望这足够简单理解【参考方案12】:

基于 vartec 的回答:

imports sys

def redirect_output(func, output=None):
    if output is None:
        output = sys.stderr
    if isinstance(output, basestring):
        output = open(output, 'w') # etc...
    # everything else...

【讨论】:

这不能用作问题中@redirect_output("somewhere.log") def foo() 示例中的装饰器。【参考方案13】:

您是否尝试过使用默认值的关键字参数?类似的东西

def decorate_something(foo=bar, baz=quux):
    pass

【讨论】:

【参考方案14】:

通常你可以在 Python 中给出默认参数...

def redirect_output(fn, output = stderr):
    # whatever

但不确定这是否也适用于装饰器。我不知道为什么它不会。

【讨论】:

如果你说@dec(abc) 函数不会直接传递给dec。 dec(abc) 返回一些东西,这个返回值被用作装饰器。因此 dec(abc) 必须返回一个函数,然后该函数将修饰函数作为参数传递。 (另见 thobes 代码)

以上是关于如何创建一个可以使用或不使用参数的 Python 装饰器?的主要内容,如果未能解决你的问题,请参考以下文章

如何定义带参数或不带参数调用的函数? [复制]

如何运行显示控制台窗口或不基于参数参数的dotnet Core控制台应用程序?

python如何新建py文件

ios如何定义函数时可以传一个参数或多个参数或不传 我记得有个变量名加到函数名后边就能实现这个效果 不

PHP 类实例化。使用或不使用括号? [关闭]

Python:如何在创建 RecycleView 时访问自定义参数?