我想创建一个可以与参数一起使用的Python装饰器:
@redirect_output("somewhere.log")
def foo():
....
或不使用它们(例如,默认情况下将输出重定向到stderr):
@redirect_output
def foo():
....
那有可能吗?
请注意,我不是在寻找重定向输出问题的其他解决方案,它只是我想要实现的语法的一个示例。
看起来默认的@redirect_output毫无意义。 id建议这是一个坏主意。 使用第一种形式可以大大简化您的生活。
但是,一个有趣的问题是-直到我看到它并仔细阅读了文档,id才假设@f与@f()相同,但老实说,我仍然认为应该如此(任何提供的参数都应附加到 函数参数)
我知道这个问题很旧,但是有些评论是新的,虽然所有可行的解决方案本质上都是相同的,但大多数解决方案都不是很干净也不易于阅读。
就像thobe的回答说的那样,处理这两种情况的唯一方法是检查这两种情况。最简单的方法是简单地检查是否有单个参数并且它是马蹄莲(注意:如果您的装饰器仅接受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)不起作用。
您需要检测这两种情况,例如使用第一个参数的类型,并相应地返回包装器(在不带参数的情况下使用)或修饰符(在与参数一起使用时)。
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")就是这种情况。令人费解,但是真的:-)
我知道这是一个老问题,但是我真的不喜欢所建议的任何技术,因此我想添加另一种方法。我看到django在django.contrib.auth.decorators的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
我发现django使用的这种方法比这里提出的任何其他技术更优雅,更易于理解。
根据是否给其参数,以根本不同的方式调用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)
这里的几个答案已经很好地解决了您的问题。但是,关于样式,我更喜欢使用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中现场演示了其中一些内容(还有更多内容!)。
快乐的编码:)
实际上,@ 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
谢谢。 这很有帮助!
要给出比以上更完整的答案:
"Is there a way to build a decorator that can be used both with and without arguments ?"
不,没有通用的方法,因为目前python语言中缺少某些东西来检测两个不同的用例。
但是,如其他答案(如bj0所指出的),是一个笨拙的解决方法,它检查收到的第一个位置参数的类型和值(并检查是否没有其他参数具有非默认值)。如果保证用户永远不会将可调用对象作为装饰器的第一个参数传递,则可以使用此替代方法。请注意,这对于类修饰器是相同的(替换前一句中的可按类调用)。
为确保上述内容,我在那里进行了大量研究,甚至实现了一个名为decopatch的库,该库使用上述所有策略(以及更多的方法,包括自省)的组合来执行"无论是最智能的解决方法"。
但坦率地说,最好的办法是在这里不需要任何库,直接从python语言获得该功能。如果像我一样,您认为很遗憾的是,Python语言至今还不能为该问题提供一个简洁的答案,请毫不犹豫地在python bugtracker中支持此想法:https://bugs.python .org / issue36553!
非常感谢您的帮助,使python成为更好的语言:)
您是否尝试过使用默认值作为关键字参数?就像是
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...
不能像问题中的@redirect_output("somewhere.log") def foo()示例中那样用作装饰器。
通常,您可以在Python中提供默认参数...
def redirect_output(fn, output = stderr):
# whatever
不过,不确定是否可以与装饰器一起使用。我不知道为什么不会。
装饰器是函数。 默认参数有效
如果您说@dec(abc),则该函数不会直接传递给dec。 dec(abc)返回某值,并且此返回值用作装饰器。 因此,dec(abc)必须返回一个函数,该函数然后将经过修饰的函数作为参数传递。 (另请参阅thobes代码)