如何创建一个可以带或不带参数使用的 Python 装饰器?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/653368/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to create a Python decorator that can be used either with or without parameters?
提问by elifiner
I'd like to create a Python decorator that can be used either with parameters:
我想创建一个可以与参数一起使用的 Python 装饰器:
@redirect_output("somewhere.log")
def foo():
....
or without them (for instance to redirect the output to stderr by default):
或者没有它们(例如,默认情况下将输出重定向到 stderr):
@redirect_output
def foo():
....
Is that at all possible?
这可能吗?
Note that I'm not looking for a different solution to the problem of redirecting output, it's just an example of the syntax I'd like to achieve.
请注意,我不是在寻找重定向输出问题的不同解决方案,这只是我想要实现的语法示例。
采纳答案by bj0
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.
我知道这个问题很老,但有些评论是新的,虽然所有可行的解决方案本质上都是一样的,但大多数都不是很干净或易于阅读。
Like thobe's answer says, the only way to handle both cases is to check for both scenarios. The easiest way is simply to check to see if there is a single argument and it is callabe (NOTE: extra checks will be necessary if your decorator only takes 1 argument and it happens to be a callable object):
就像 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)
In the first case, you do what any normal decorator does, return a modified or wrapped version of the passed in function.
在第一种情况下,您执行任何普通装饰器所做的操作,返回传入函数的修改或包装版本。
In the second case, you return a 'new' decorator that somehow uses the information passed in with *args, **kwargs.
在第二种情况下,您返回一个“新”装饰器,它以某种方式使用通过 *args、**kwargs 传入的信息。
This is fine and all, but having to write it out for every decorator you make can be pretty annoying and not as clean. Instead, it would be nice to be able to automagically modify our decorators without having to re-write them... but that's what decorators are for!
这很好,但是必须为您制作的每个装饰器写出来可能会非常烦人而且不那么干净。相反,能够自动修改我们的装饰器而不必重新编写它们会很好……但这就是装饰器的用途!
Using the following decorator decorator, we can deocrate our decorators so that they can be used with or without arguments:
使用以下装饰器装饰器,我们可以对装饰器进行 deocrate,以便它们可以带参数或不带参数使用:
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
Now, we can decorate our decorators with @doublewrap, and they will work with and without arguments, with one caveat:
现在,我们可以用@doublewrap 装饰我们的装饰器,它们可以使用和不使用参数,但有一个警告:
I noted above but should repeat here, the check in this decorator makes an assumption about the arguments that a decorator can receive (namely that it can't receive a single, callable argument). Since we are making it applicable to any generator now, it needs to be kept in mind, or modified if it will be contradicted.
我在上面提到过,但应该在这里重复,这个装饰器中的检查对装饰器可以接收的参数做出了假设(即它不能接收单个可调用的参数)。由于我们现在使其适用于任何生成器,因此需要牢记它,或者在与它发生矛盾时进行修改。
The following demonstrates its use:
下面演示了它的用法:
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
回答by thobe
Using keyword arguments with default values (as suggested by kquinn) is a good idea, but will require you to include the parenthesis:
使用带有默认值的关键字参数(如 kquinn 所建议的)是一个好主意,但需要您包含括号:
@redirect_output()
def foo():
...
If you would like a version that works without the parenthesis on the decorator you will have to account both scenarios in your decorator code.
如果你想要一个在装饰器上没有括号的版本,你必须在你的装饰器代码中考虑这两种情况。
If you were using Python 3.0 you could use keyword only arguments for this:
如果您使用的是 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)
In Python 2.x this can be emulated with varargs tricks:
在 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
Any of these versions would allow you to write code like this:
这些版本中的任何一个都允许您编写如下代码:
@redirected_output
def foo():
...
@redirected_output(destination="somewhere.log")
def bar():
...
回答by Remy Blank
You need to detect both cases, for example using the type of the first argument, and accordingly return either the wrapper (when used without parameter) or a decorator (when used with arguments).
您需要检测这两种情况,例如使用第一个参数的类型,并相应地返回包装器(不带参数使用时)或装饰器(带参数使用时)。
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
When using the @redirect_output("output.log")
syntax, redirect_output
is called with a single argument "output.log"
, and it must return a decorator accepting the function to be decorated as an argument. When used as @redirect_output
, it is called directly with the function to be decorated as an argument.
使用@redirect_output("output.log")
语法时,redirect_output
使用单个参数调用"output.log"
,它必须返回一个装饰器,接受要装饰的函数作为参数。使用 as 时@redirect_output
,直接将要装饰的函数作为参数调用。
Or in other words: the @
syntax must be followed by an expression whose result is a function accepting a function to be decorated as its sole argument, and returning the decorated function. The expression itself can be a function call, which is the case with @redirect_output("output.log")
. Convoluted, but true :-)
或者换句话说:@
语法后面必须跟一个表达式,其结果是一个函数,接受一个被装饰的函数作为它的唯一参数,并返回被装饰的函数。表达式本身可以是函数调用,@redirect_output("output.log")
. 令人费解,但确实如此:-)
回答by dgel
I know this is an old question, but I really don't like any of the techniques proposed so I wanted to add another method. I saw that django uses a really clean method in their login_required
decorator in django.contrib.auth.decorators
. As you can see in the decorator's docs, it can be used alone as @login_required
or with arguments, @login_required(redirect_field_name='my_redirect_field')
.
我知道这是一个老问题,但我真的不喜欢提出的任何技术,所以我想添加另一种方法。我看到 django 在他们的login_required
装饰器中django.contrib.auth.decorators
使用了一种非常干净的方法。正如您在装饰器的文档中所见,它可以单独用作@login_required
或与参数@login_required(redirect_field_name='my_redirect_field')
.
The way they do it is quite simple. They add a kwarg
(function=None
) before their decorator arguments. If the decorator is used alone, function
will be the actual function it is decorating, whereas if it is called with arguments, function
will be None
.
他们的做法很简单。他们在装饰器参数之前添加kwarg
( function=None
) 。如果单独使用装饰器,function
它将是它正在装饰的实际函数,而如果使用参数调用它,function
则将是None
.
Example:
例子:
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.
我发现 django 使用的这种方法比这里提出的任何其他技术都更优雅、更容易理解。
回答by rog
A python decorator is called in a fundamentally different way depending on whether you give it arguments or not. The decoration is actually just a (syntactically restricted) expression.
python 装饰器的调用方式完全不同,具体取决于您是否为其提供参数。装饰实际上只是一个(受语法限制的)表达式。
In your first example:
在你的第一个例子中:
@redirect_output("somewhere.log")
def foo():
....
the function redirect_output
is called with the
given argument, which is expected to return a decorator
function, which itself is called with foo
as an argument,
which (finally!) is expected to return the final decorated function.
该函数redirect_output
被调用,给定的参数,预计将返回一个装饰作用,它本身被调用,foo
作为参数,这(最后!)预计将返回最终的装饰功能。
The equivalent code looks like this:
等效代码如下所示:
def foo():
....
d = redirect_output("somewhere.log")
foo = d(foo)
The equivalent code for your second example looks like:
第二个示例的等效代码如下所示:
def foo():
....
d = redirect_output
foo = d(foo)
So you cando what you'd like but not in a totally seamless way:
所以你可以做你想做的事,但不是完全无缝的:
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)
This should be ok unless you wish to use a function as an argument to your decorator, in which case the decorator will wrongly assume it has no arguments. It will also fail if this decoration is applied to another decoration that does not return a function type.
这应该没问题,除非您希望将函数用作装饰器的参数,在这种情况下,装饰器会错误地假定它没有参数。如果将此装饰应用于另一个不返回函数类型的装饰,它也会失败。
An alternative method is just to require that the decorator function is always called, even if it is with no arguments. In this case, your second example would look like this:
另一种方法是要求始终调用装饰器函数,即使它没有参数。在这种情况下,您的第二个示例如下所示:
@redirect_output()
def foo():
....
The decorator function code would look like this:
装饰器函数代码如下所示:
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)
回答by henrywallace
Several answers here already address your problem nicely. With respect to style, however, I prefer solving this decorator predicament using functools.partial
, as suggested in David Beazley's Python Cookbook 3:
这里的几个答案已经很好地解决了您的问题。然而,在风格方面,我更喜欢使用 来解决这个装饰器困境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
While yes, you can just do
虽然是的,你可以做
@decorator()
def f(*args, **kwargs):
pass
without funky workarounds, I find it strange looking, and I like having the option of simply decorating with @decorator
.
没有时髦的解决方法,我觉得它看起来很奇怪,而且我喜欢选择简单地用@decorator
.
As for the secondary mission objective, redirecting a function's output is addressed in this Stack Overflow post.
至于次要任务目标,重定向函数的输出在这篇Stack Overflow 帖子中得到解决。
If you want to dive deeper, check out Chapter 9 (Metaprogramming) in Python Cookbook 3, which is freely available to be read online.
如果您想深入了解,请查看Python Cookbook 3 中的第 9 章(元编程),该书可免费在线阅读。
Some of that material is live demoed (plus more!) in Beazley's awesome YouTube video Python 3 Metaprogramming.
其中一些材料在 Beazley 的精彩 YouTube 视频Python 3 元编程 中进行了现场演示(还有更多!)。
Happy coding :)
快乐编码:)
回答by Ainz Titor
In fact, the caveat case in @bj0's solution can be checked easily:
实际上,可以轻松检查@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
Here are a few test cases for this fail-safe version of 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
回答by smarie
To give a more complete answer than the above:
给出比上述更完整的答案:
"Is there a way to build a decorator that can be used both with and without arguments ?"
“有没有办法构建一个既可以使用也可以不使用参数的装饰器?”
Nothere is no generic way because there is currently something missing in the python language to detect the two different use cases.
不,没有通用的方法,因为 Python 语言中目前缺少一些东西来检测两种不同的用例。
However Yesas already pointed out by other answers such as bj0
s, there is a clunky workaroundthat is to check the type and value of the first positional argument received (and to check if no other arguments have non-default value). If you are guaranteed that users will neverpass a callable as first argument of your decorator, then you can use this workaround. Note that this is the same for class decorators (replace callable by class in the previous sentence).
不过是因为已经通过其他的答案,如指出的bj0
小号,还有一个笨重的解决方法是检查收到的第一个位置参数的类型和值(和检查,如果没有其他参数没有默认值)。如果您保证用户永远不会将 callable 作为装饰器的第一个参数传递,那么您可以使用此解决方法。请注意,这对于类装饰器也是一样的(替换上一句中的 callable by class)。
To be sure of the above, I did quite a bit of research out there and even implemented a library named decopatch
that uses a combination of all strategies cited above (and many more, including introspection) to perform "whatever is the most intelligent workaround" depending on your need.
为了确保上述内容,我在那里进行了大量研究,甚至实现了一个名为的库decopatch
,该库使用上述所有策略的组合(以及更多策略,包括内省)来执行“任何最智能的解决方法”,具体取决于根据您的需要。
But frankly the best would be not to need any library here and to get that feature straight from the python language. If, like myself, you think that it is a pity that the python language is not as of today capable of providing a neat answer to this question, do not hesitate to support this idea in the python bugtracker: https://bugs.python.org/issue36553!
但坦率地说,最好的办法是这里不需要任何库并直接从 python 语言中获得该功能。如果像我一样,你认为很遗憾 Python 语言目前无法为这个问题提供一个简洁的答案,请不要犹豫,在 python bugtracker 中支持这个想法:https://bugs.python .org/issue36553!
Thanks a lot for your help making python a better language :)
非常感谢您帮助使 Python 成为更好的语言 :)
回答by j4hangir
This does the job without no fuss:
这可以毫不费力地完成工作:
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
回答by kquinn
Have you tried keyword arguments with default values? Something like
您是否尝试过使用默认值的关键字参数?就像是
def decorate_something(foo=bar, baz=quux):
pass