Python 现代开发学习笔记

(十四)用装饰器包装已有函数

1. 先把包装动作写完整

现在想在若干操作前后显示“开始”和“结束”,但不想把这些输出写进每个计算函数。前面已经能传入函数、返回闭包,可以把这两件事组合起来:

# traced 接收原函数,返回一个代它接受调用的包装函数。
def traced(func):
    def wrapper(*args, **kwargs):
        print("开始", func.__name__)
        result = func(*args, **kwargs)
        print("结束", func.__name__)
        return result
    return wrapper


def total_minutes(values):
    return sum(values)


# 先保留原函数,再让原来的名称指向包装函数。
original = total_minutes
total_minutes = traced(original)
print(total_minutes([20, 35]))

输出“开始 total_minutes”“结束 total_minutes”“55”。func.__name__ 读取函数对象保存的名称。调用者实际进入 wrapper,它通过闭包保存的 func 找到原函数,再把参数拆开交给原函数。最后把 result 返回给调用者,计算结果没有在包装层丢失。

装饰时保存原函数,调用时经过包装层

traced(original) 在建立包装时执行;wrapper(...) 在日后调用时执行。包装中的 *args 与 **kwargs 分别收集调用参数,func(*args, **kwargs) 再把它们拆开。这正是第二节中两种方向的星号用法。

2. @ 写法与 functools.wraps

Python 把上面的重新绑定过程提供成 @ 写法。@traced 放在函数定义前,建立原函数后把它交给 traced,再把返回对象绑定到原来的函数名。

# 从标准库 functools 导入 wraps,用来保留原函数的名称、说明等信息。
from functools import wraps


def traced(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("开始", func.__name__)
        result = func(*args, **kwargs)
        print("结束", func.__name__)
        return result
    return wrapper


@traced
def total_minutes(values):
    """合计阅读分钟数。"""
    return sum(values)


print(total_minutes([20, 35]))
print(total_minutes.__name__)
print(total_minutes.__doc__)

最后两行是 total_minutes 和“合计阅读分钟数。”。不使用 wraps 时,重新绑定后的名称实际上指向 wrapper,从它读取 __name__ 会得到 "wrapper"。wraps(func) 返回一个装饰器,把原函数的元信息安排到包装函数上,并保存 __wrapped__ 供工具找到原函数;真正运行的仍是包装层。wraps 说明

上面的“结束”输出在原函数正常返回后执行。如果原函数在中途抛出异常,控制不会继续走到那条 print;异常与 finally 会在文件章节中结合资源处理展开。这里先沿一次成功调用看清参数和结果经过的位置。

3. 带参数的装饰器与叠放顺序

希望给不同的包装层取名字,就再增加一层接收配置的函数:

# tag(label) 先保存标签,返回真正接收原函数的 decorate。
def tag(label):
    def decorate(func):
        def wrapper(*args, **kwargs):
            print(label, "进入")
            result = func(*args, **kwargs)
            print(label, "离开")
            return result
        return wrapper
    return decorate


@tag("外层")
@tag("内层")
def show_total():
    print("计算正文")
    return 55


print(show_total())

输出依次是“外层 进入”“内层 进入”“计算正文”“内层 离开”“外层 离开”“55”。先把原函数交给靠近它的内层装饰器,再把得到的包装函数交给外层:show_total = tag("外层")(tag("内层")(原函数))。调用时从最外层进入,返回时按相反方向退出。装饰器的应用顺序

装饰器叠放的建立顺序与调用顺序

装饰器的用途由接收函数的那段代码决定:它可以返回包装函数,也可以登记函数后原样返回。后面框架里的 @... 写法,要继续看装饰器做了什么,不能单凭符号判断它一定多套了一层调用。

笔记正文与全部题解 · Ctrl / ⌘ K