如何创build一个可以使用或不使用参数的Python装饰器?
我想创build一个Python装饰器,可以使用参数:
@redirect_output("somewhere.log") def foo(): ....
或者没有它们(例如默认情况下将输出redirect到stderr):
@redirect_output def foo(): ....
这是可能的吗?
请注意,我不是在寻找redirect输出问题的另一种解决scheme,它只是我想要达到的语法的一个例子。
我知道这个问题是旧的,但一些评论是新的,虽然所有可行的解决scheme基本相同,但其中大部分都不是很干净或容易阅读。
就像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)
在第一种情况下,你可以做任何普通的装饰器所做的事情,返回传入函数的修改或包装版本。
在第二种情况下,您返回一个“新”装饰器,它以某种方式使用传递给* 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所build议的)是一个好主意,但是需要包含括号:
@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(): ...
您需要检测这两种情况,例如使用第一个参数的types,并相应地返回包装(不带参数的情况下)或装饰器(与参数一起使用时)。
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
,直接调用函数作为参数进行修饰。
换句话说: @
语法后面必须跟一个expression式,其结果是一个函数接受一个函数作为唯一参数进行装饰,并返回装饰的函数。 expression式本身可以是一个函数调用,这是@redirect_output("output.log")
。 令人费解,但真实:-)
一个python装饰器是根据你是否给它的参数而从根本上不同的方式调用的。 装饰实际上只是一个(语法限制的)expression。
在你的第一个例子中:
@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)
这应该是可以的,除非你想使用一个函数作为你的装饰器的参数,在这种情况下,装饰器会错误地认为它没有参数。 如果将此装饰应用于不返回functiontypes的其他装饰,则也会失败。
另一种方法就是要求装饰器函数总是被调用,即使它没有参数。 在这种情况下,你的第二个例子看起来像这样:
@redirect_output() def foo(): ....
装饰器function代码如下所示:
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)
我知道这是一个古老的问题,但我真的不喜欢任何提出的技术,所以我想添加另一种方法。 我看到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使用的这个方法比这里提出的其他技术更优雅,更容易理解。
这里有几个答案已经很好地解决了你的问题。 然而,就风格而言,我更喜欢使用functools.partial
解决这个装饰困境,正如David Beazley的Python Cookbook 3所build议的:
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
装饰的选项。
至于第二个任务目标,redirect一个函数的输出在这个堆栈溢出post中解决 。
如果你想深入了解一下,可以看看Python Cookbook 3中的第9章(元编程),它可以免费在线阅读 。
其中一些素材是Beazley的YouTubevideoPython 3 Metaprogramming中的现场演示(还有更多!)。
快乐编码:)
事实上,在@ bj0的解决scheme中的警告情况可以很容易地检查:
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
。
@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
你有没有尝试使用默认值的关键字参数? 就像是
def decorate_something(foo=bar, baz=quux): pass
一般来说,你可以给Python的默认参数…
def redirect_output(fn, output = stderr): # whatever
不知道是否也可以与装饰器一起使用。 我不知道为什么它不会。
build立在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...