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

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

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

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

@redirect_output
def foo():
....

这可能吗?

注意,我并不是在寻找重定向输出问题的不同解决方案,这只是我希望实现的语法的一个示例。

26543 次浏览

通常,您可以在 Python 中给出默认参数..。

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

不过,我不确定这是否也适用于室内设计师,我不知道有什么理由不适用。

你有没有试过使用默认值的关键字参数

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

基于 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...

使用带默认值的关键字参数(正如 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():
...

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

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")语法时,使用单个参数 "output.log"调用 redirect_output,并且它必须返回一个修饰符,以接受要作为参数修饰的函数。当作为 @redirect_output使用时,它会被作为参数直接调用。

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

根据您是否为其提供参数,将以完全不同的方式调用 python 装饰器。装饰实际上只是一个(语法受限制的)表达式。

在你的第一个例子中:

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

the function redirect_output is called with the 给定参数,该参数期望返回一个修饰符 函数,它本身是用 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)

这应该可以,除非您希望将函数用作 你的装潢师,在这种情况下,装潢师 will wrongly assume it has no arguments. It will also fail if this decoration is applied to another decoration that 不返回函数类型。

另一种方法是要求 decorator function is always called, even if it is with no arguments. 在这种情况下,您的第二个示例如下所示:

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

I know this question is old, but some of the comments are new, and while all of the viable solutions are essentially the same, most of them aren't very clean or easy to read.

正如 Thobe 的回答所说,处理这两种情况的唯一方法是同时检查两种情况。最简单的方法是简单地检查是否有一个参数和它是 callabe (注意: 额外的检查将是必要的,如果你的装饰器只有一个参数和它碰巧是一个可调用的对象) :

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)

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

In the second case, you return a 'new' decorator that somehow uses the information passed in with *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

现在,我们可以用@doublewrapp 来装饰我们的装潢师,他们可以使用或不使用参数,但有一点需要注意:

我已经注意到了上面的内容,但是应该在这里重复一下,这个装饰器中的检查对装饰器可以接收的参数做了一个假设(即它不能接收单个可调用的参数)。因为我们正在使它适用于任何发电机现在,它需要被记住,或修改,如果它将被抵触。

以下说明其用途:

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

我知道这是一个老问题,但我真的不喜欢所提出的任何技术,所以我想添加另一种方法。我看到 django 在他们的 ABC1中的 login_required装饰器中使用了一个非常干净的方法。正如您在 装潢师的文件中所看到的,它可以单独用作 @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

I find this approach that django uses to be more elegant and easier to understand than any of the other techniques proposed here.

事实上,可以很容易地检查@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

这里的几个答案已经很好地解决了你的问题。然而,就风格而言,我更喜欢使用 functools.partial来解决这个装饰师的困境,就像大卫 · 比兹利的 Python 烹饪书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

As for the secondary mission objective, redirecting a function's output is addressed in this Stack Overflow post.


如果你想深入了解,请查看 Python 烹饪书3中的第9章(元编程) ,它是免费提供给 在线阅读的。

其中一些材料在比兹利的优秀 YouTube 视频 Python 3 Metaprogramming中进行了现场演示(还有更多!)。

完成其他答案:

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

No 没有通用的方法,因为目前在 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 bug 跟踪器中支持这个想法: https://bugs.python.org/issue36553

这种方法毫不费力地完成了任务:

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

由于没有人提到这一点,还有一个解决方案,利用可调用的类,我觉得更优雅,特别是在这种情况下,修饰器是复杂的,人们可能希望将它拆分为多个方法(函数)。这个解决方案基本上是利用 __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__返回任何其他值

警告

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

这对我有用:

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

下面的示例代码 multiply()可以接受修饰符中的一个参数或不接受括号,而下面的 sum()可以求和2个数字:

from numbers import Number


def multiply(num):
def _multiply(func):
def core(*args, **kwargs):
result = func(*args, **kwargs)
if isinstance(num, Number):
return result * num
else:
return result
return core
if callable(num):
return _multiply(num)
else:
return _multiply


def sum(num1, num2):
return num1 + num2

因此,如果你把 @multiply(5)放在 sum()上,然后调用 sum(4, 6),如下所示:

# (4 + 6) x 5 = 50


@multiply(5) # Here
def sum(num1, num2):
return num1 + num2


result = sum(4, 6)
print(result)

You can get the result below:

50

如果你把 @multiply放在 sum()上,然后调用 sum(4, 6),如下所示:

# 4 + 6 = 10


@multiply # Here
def sum(num1, num2):
return num1 + num2
    

result = sum(4, 6)
print(result)

你可以得到以下结果:

10

但是,如果你把 @multiply()放在 sum()上,然后调用 sum(4, 6),如下所示:

@multiply() # Here
def sum(num1, num2):
return num1 + num2
    

result = sum(4, 6)
print(result)

以下错误发生:

TypeError: 乘()缺少1个必需的位置参数: ‘ num’

So, if you want @multiply() to run without error, you need to add 默认值 1 to num as shown below:

from numbers import Number
# Here
def multiply(num=1):
def _multiply(func):
def core(*args, **kwargs):
# ...

然后,如果你把 @multiply()放在 sum()上,然后调用 sum(4, 6),如下所示:

# (4 + 6) x 1 = 10


@multiply() # Here
def sum(num1, num2):
return num1 + num2
    

result = sum(4, 6)
print(result)

You can get the result below:

10