(十四)用装饰器包装已有函数
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("内层")(原函数))。调用时从最外层进入,返回时按相反方向退出。装饰器的应用顺序
装饰器的用途由接收函数的那段代码决定:它可以返回包装函数,也可以登记函数后原样返回。后面框架里的 @... 写法,要继续看装饰器做了什么,不能单凭符号判断它一定多套了一层调用。