我想创建一个可以与参数一起使用的Python装饰器:
@redirect_output("somewhere.log")
def foo():
....
或不使用它们(例如,默认情况下将输出重定向到stderr):
@redirect_output
def foo():
....
那有可能吗?
请注意,我并不是在寻找重定向输出问题的其他解决方案,它只是我想要实现的语法的一个示例。
Answers:
我知道这个问题很旧,但是有些评论是新的,尽管所有可行的解决方案本质上都是相同的,但大多数解决方案都不是很干净也不易于阅读。
就像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
将关键字参数与默认值一起使用(如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():
...
your code here
什么?您如何称呼装饰的功能?fn(*args, **kwargs)
不起作用。
def f(a = 5): return MyDecorator( a = a)
和 class MyDecorator( object ): def __init__( self, a = 5 ): ....
对不起它很难写它在一个评论,但我希望这是很简单的理解
我知道这是一个老问题,但是我真的不喜欢所建议的任何技术,因此我想添加另一种方法。我看到django在的login_required
装饰器中django.contrib.auth.decorators
使用了一种非常干净的方法。正如您在装饰器的文档中看到的那样,它可以单独用作@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使用的这种方法比这里提出的任何其他技术更优雅,更易于理解。
function
,然后事情就会中断,因为装饰器会尝试像对待装饰函数一样调用第一个位置arg。
您需要检测这两种情况,例如使用第一个参数的类型,并相应地返回包装器(在不带参数的情况下使用)或修饰符(在与参数一起使用时)。
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")
。令人费解,但是真的:-)
这里的几个答案已经很好地解决了您的问题。但是,关于样式,我更喜欢使用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帖子中介绍了重定向功能输出的问题。
如果您想更深入地学习,请查阅Python Cookbook 3中的第9章(元编程),该手册可免费在线阅读。
Beazley很棒的YouTube视频Python 3 Metaprogramming中现场演示了其中的一些内容(还有更多内容!)。
快乐的编码:)
根据是否给其参数,以根本不同的方式调用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)
实际上,@ 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
要给出比以上更完整的答案:
“有没有一种方法可以构建既可以带参数也可以不带参数的装饰器?”
不,没有通用的方法,因为python语言目前缺少某些东西来检测两个不同的用例。
不过是因为已经通过其他的答案,如指出的bj0
小号,还有一个笨重的解决方法是检查收到的第一个位置参数的类型和值(和检查,如果没有其他参数没有默认值)。如果保证用户永远不会将可调用对象作为装饰器的第一个参数传递,则可以使用此替代方法。请注意,这对于类装饰器是相同的(替换前一句中的可按类调用)。
为确保上述内容,我在那里进行了大量研究,甚至实现了一个名为的库decopatch
,该库使用上述所有策略(以及更多的方法,包括自省)的组合来执行“无论哪种方法都最明智”根据您的需要。
但坦率地说,最好的办法是这里不需要任何库,直接从python语言获得该功能。如果像我一样,您认为python语言到今天还没有能力为这个问题提供一个简洁的答案是可惜的,请毫不犹豫地在python bugtracker中支持这个想法:https://bugs.python .org / issue36553!
非常感谢您的帮助,使python成为更好的语言:)
由于没有人提及,因此还有一种使用可调用类的解决方案,我发现它更优雅,特别是在装饰器很复杂并且可能希望将其拆分为多个方法(函数)的情况下。该解决方案利用__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__
以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()
问题示例中那样用作装饰器。
@redirect_output
很默认的消息是毫无根据的。我建议这是一个坏主意。使用第一种形式可以大大简化您的生活。