python无参数装饰器_如何创建可以使用或不使用参数的Python装饰器?

我想创建一个可以与参数一起使用的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代码)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值