Python 是否允许调用装饰器工厂?

Does Python allow calls to decorator factory factories?

Ian McCracken's blog, he has an article where he talks about decorator factory factories。在文章中,他举了一个例子:

def decorator_factory_factory(method):
    def decorator_factory(regex):
        def decorator(f):
            def inner(*args, **kwargs):
                # do stuff with f(*args, **kwargs)
                # and method and regex
            return inner
        return decorator
    return decorator_factory

然后他举例说明了如何调用装饰器:

@decorator_factory_factory('GET')('^/.*$')
def onGetAnything(self):
    pass

这引起了我的注意。我以前从未尝试过调用装饰器装饰器工厂,所以我决定看看代码的行为方式:

>>> def decorator_factory_factory(method):
    def decorator_factory(regex):
        def decorator(f):
            def inner(*args, **kwargs):
                print(args, kwargs)
            return inner
        return decorator
    return decorator_factory

>>> @decorator_factory_factory('GET')('^/.*$')
def onGetAnything(self):
    pass
SyntaxError: invalid syntax
>>> 

从上面可以看出,Python引发了一个SyntaxError。这是为什么?除了明显适用于 McCraken 先生的代码之外,这样的代码似乎 运行 完全没问题。它不是与将函数调用链接在一起的基本相同的语法吗?例如:

>>> def foo():
    def bar():
        return 2
    return bar

>>> foo()()
2
>>>

我想也许他使用的是旧版本的 Python 允许这样的语法,所以我查看了他回写文章时可能使用的 Python 版本的语法在2009年; 2.6.9.但是 the grammar 似乎仍然不允许链式装饰器调用:

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef)

any Python 版本是否允许使用此语法。如果不是,那么 Ian 是如何 运行 他的代码的?他只是犯了一个错误吗?

作为变通方法,要使其正常工作,您可以创建一个 decorator_caller 装饰器,它将为您进行调用:

def decorator_caller(decorator, args_list):
    d = decorator
    for args in args_list:
        d = d(*args)
    return d

这样使用:

@decorator_caller(decorator_factory_factory, (('GET',), ('^/.*$',)))
def onGetAnything(self):
    pass

或者改用列表(因为单项元组看起来很混乱):

@decorator_caller(decorator_factory_factory, (['GET'], ['^/.*$']))
def onGetAnything(self):
    pass

注意,我不相信这样的工厂工厂是建起来有用的东西。当外部工厂真正影响构建的内部工厂的逻辑时,工厂工厂就有意义了。当你所做的只是创建一个临时工厂来创建单个项目只是为了立即丢弃工厂时,那么除了更复杂和更差的整体性能之外,你并没有真正获得任何好处。

如果您实际上将工厂存放在周围以供重复使用,这将是另一回事:

getFactory = decorator_factory_factory('GET')

@getFactory('^/index.*$')
def index():
    pass

@getFactory('^/.*$')
def x():
    pass

这是有道理的,语法也可以正常工作。但是如果你只是想链接工厂调用以添加另一个参数来配置装饰器,那么你应该调整工厂以接受这两个参数。