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


88

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

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

或不使用它们(例如,默认情况下将输出重定向到stderr):

@redirect_output
def foo():
    ....

那有可能吗?

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


看起来@redirect_output很默认的消息是毫无根据的。我建议这是一个坏主意。使用第一种形式可以大大简化您的生活。
S.Lott

有趣的问题-在我看到并仔细阅读文档之前,我一直假设@f与@f()相同,老实说,我仍然认为应该如此(任何提供的参数都将被处理)转到函数参数)
rog

Answers:


63

我知道这个问题很旧,但是有些评论是新的,尽管所有可行的解决方案本质上都是相同的,但大多数解决方案都不是很干净也不易于阅读。

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

31

将关键字参数与默认值一起使用(如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中,可以用varargs技巧来模拟:

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():
    ...

1
你放your code here什么?您如何称呼装饰的功能?fn(*args, **kwargs)不起作用。
12年

我认为有一个更简单的答案,创建一个类,该类将带有可选参数的装饰器。使用具有默认默认值的相同参数创建另一个函数,并返回装饰器类的新实例。应该是这个样子:def f(a = 5): return MyDecorator( a = a) class MyDecorator( object ): def __init__( self, a = 5 ): .... 对不起它很难写它在一个评论,但我希望这是很简单的理解
奥马尔本哈伊姆

17

我知道这是一个老问题,但是我真的不喜欢所建议的任何技术,因此我想添加另一种方法。我看到django在的login_required装饰器中django.contrib.auth.decorators使用了一种非常干净的方法。正如您在装饰器的文档中看到的那样,它可以单独用作@login_required参数或与参数一起使用@login_required(redirect_field_name='my_redirect_field')

他们这样做的方式非常简单。他们在装饰器参数之前添加kwargfunction=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。
达斯汀·怀亚特

12

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

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")。令人费解,但是真的:-)


9

这里的几个答案已经很好地解决了您的问题。但是,关于样式,我更喜欢使用functools.partialDavid 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帖子中介绍了重定向功能输出的问题。


如果您想更深入地学习,请查阅Python Cookbook 3中的第9章(元编程),该手册可免费在线阅读

Beazley很棒的YouTube视频Python 3 Metaprogramming中现场演示了其中的一些内容(还有更多内容!)。

快乐的编码:)


8

根据是否给其参数,以根本不同的方式调用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)

2

实际上,@ 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

1
谢谢。这很有帮助!
Pragy Agarwal

0

要给出比以上更完整的答案:

“有没有一种方法可以构建既可以带参数也可以不带参数的装饰器?”

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

不过因为已经通过其他的答案,如指出的bj0小号,还有一个笨重的解决方法是检查收到的第一个位置参数的类型和值(和检查,如果没有其他参数没有默认值)。如果保证用户永远不会将可调用对象作为装饰器的第一个参数传递,则可以使用此替代方法。请注意,这对于类装饰器是相同的(替换前一句中的可按类调用)。

为确保上述内容,我在那里进行了大量研究,甚至实现了一个名为的库decopatch,该库使用上述所有策略(以及更多的方法,包括自省)的组合来执行“无论哪种方法都最明智”根据您的需要。

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

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


0

这样就可以做到:

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

0

由于没有人提及,因此还有一种使用可调用类的解决方案,我发现它更优雅,特别是在装饰器很复杂并且可能希望将其拆分为多个方法(函数)的情况下。该解决方案利用__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的调用__call__则完全被省略。这就是为什么我们需要采用逻辑__new__方法以魔术方法处理这种情况。

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


-1

您是否尝试过使用具有默认值的关键字参数?就像是

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

-2

通常,您可以在Python中提供默认参数...

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

不过,不确定是否可以与装饰器一起使用。我不知道为什么不会。


2
如果您说@dec(abc),则该函数不会直接传递给dec。dec(abc)返回某值,并且此返回值用作装饰器。因此dec(abc)必须返回一个函数,然后该函数将装饰后的函数作为参数传递。(另见thobes代码)
某事

-2

以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()问题示例中那样用作装饰器。
ehabkost 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.