为什么Python装饰器无法跨定义链式调用?

8 投票
3 回答
1237 浏览
提问于 2025-04-15 18:12

为什么下面这两个脚本不一样呢?

(摘自另一个问题:理解Python装饰器

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makebold
@makeitalic
def hello():
    return "hello world"

print hello() ## returns <b><i>hello world</i></b>

还有一个被装饰的装饰器:

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

@makebold
def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makeitalic
def hello():
    return "hello world"

print hello() ## TypeError: wrapped() takes no arguments (1 given)

我为什么想知道这个呢?我写了一个叫做 retry 的装饰器,用来捕捉 MySQLdb 的异常——如果这个异常是暂时的(比如超时),它会在稍等一会儿后重新调用这个函数。

我还有一个叫 modifies_db 的装饰器,它负责一些与缓存相关的维护工作。modifies_db 是用 retry 装饰的,所以我以为所有用 modifies_db 装饰的函数也会自动重试。那我哪里搞错了呢?

3 个回答

0

这个问题是要把“makeitalic”这个函数(它需要一个参数)替换成“makebold”这个函数,而“makebold”是不需要任何参数的。

可以使用 *args, **kwargs 来把参数继续传递下去:

def wrapped(*args, **kwargs):
    return "<b>" + fn(*args, **kwargs) + "</b>"
1

原因是,makebold 里面的 wrapped() 不接受任何参数。

当你这样使用装饰器时,可能会出现一些问题。不过我会给你一个例子,告诉你怎么实现你想要的效果,稍等一下。

这里有一个你需要的工作示例。

def makebold(rewrap=False):
    if rewrap:
        def inner(decorator):
            def rewrapper(func):
                def wrapped(*args, **kwargs):
                    return "<b>%s</b>" % decorator(func)(*args,**kwargs)
                return wrapped
            return rewrapper
        return inner

    else:
        def inner(func):
            def wrapped(*args, **kwargs):
                return "<b>%s</b>" % func(*args, **kwargs)    
            return wrapped
        return inner

@makebold(rewrap=True)
def makeitalic(fn):
    def wrapped(*args, **kwargs):
        return "<i>%s</i>" % fn(*args, **kwargs)
    return wrapped

@makeitalic
def hello():
    return "hello world"

@makebold()
def hello2():
    return "Bob Dole"    

if __name__ == "__main__":
    print hello()   
    print hello2()

makebold 看起来有点复杂,但它展示了如何写一个可以选择性地包裹另一个装饰器的装饰器。

下面是上面脚本的输出结果:

<b><i>hello world</i></b>
<b>Bob Dole</b>

注意,makebold 是唯一一个递归的装饰器。还有,使用上有一个微妙的区别:@makebold()@makeitalic

9

第二个例子的问题在于

@makebold
def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

它试图装饰的是 makeitalic 这个装饰器,而不是它返回的 wrapped 这个函数。

你可以用下面这样的方式来实现我认为你想要的效果:

def makeitalic(fn):
    @makebold
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

在这里,makeitalic 使用 makebold 来装饰 wrapped

撰写回答