Python 11:装饰器改变了哪个调用对象
给任务执行入口加计时后,成功结果没有变,未知任务 ID 仍抛 KeyError,却多出两条记录:一次成功、一次失败。这比“看到耗时数字”更重要。包装层若吞掉异常、重复执行失败函数,或者令调用方再也看不到原有签名,计时功能就改变了任务合同。
在 06 的闭包基础上,tasklab.metrics.timed 接收一个由应用持有的记录列表,返回装饰函数;真正处理词频的仍是同一个 run,装饰器不重试也不替业务选择终态。
外部调用拿到的是包装后的函数
1 | |
调用被装饰的名称,进入的是 wrapper;它转发原有位置参数与关键字参数,执行 function,把函数结果直接返回。异常发生时,finally 同样写下失败记录,但没有把异常转成成功值,也没有再次调用目标函数。图中的记录完成并不表示业务成功。
@functools.wraps 保留 __name__、__wrapped__ 等元信息;Python 3.12 官方文档说明它的作用。inspect.signature 默认沿 __wrapped__ 读取原函数的签名,因此实测原函数和装饰后的 execute 签名相同。若忘了 wraps,运行结果不一定立刻错,但帮助信息和签名检查会改变;也不该因此断言所有运行时反射工具都无条件等价。
计时记录不是基准报告
perf_counter_ns() 提供适合测间隔的单调高精度时钟读数,文档界定了其用途。本实验只断言“有两条记录,间隔非负”,没有用一次计时给词频计算排性能名次。包装本身有开销,磁盘、进程、数据规模都未进入本轮对照;36 才会固定负载与重复样本做基准。
这一实现还依赖一个前提:records 是可信赖的进程内列表。若记录写入本身抛异常,finally 中的新错误可能掩盖原异常;真实监控系统要明确处理这条失败路径,本章没有把它隐藏成“无损监控”。已经失败的 run 会怎样转成任务终态,是 15 要实现的业务边界,不属于计时装饰器。
从 examples/python-language/ 运行:
1 | |
两条命令都以 0 退出,演示输出 "record_success": [true, false]、"record_count": 2、"error": "KeyError"、"signature_preserved": true。累计 34 条测试,新增用例还核对失败调用没有修改先前排队任务,不仅检查装饰器的返回值。环境和原始输出在 examples/python-language/evidence/11/20261007-decorators/。
预测题
把 finally 中记录失败的语句改成 except KeyError: return {},原有的错误调用会产生什么变化?
答案:调用方不再收到 KeyError,一个未知任务 ID 看起来像得到合法的空词频结果;这与 02、07 对成功空结果的定义冲突。test_error_is_not_swallowed_or_retried 会先失败。错误改动未应用到正式装饰器。
变更题
要对运行入口加入可选 expected= 参数,包装器只写 def wrapper(task_id): ... 会破坏什么?需要怎样检查?
答案:关键字参数可能被包装器拒绝,原函数甚至不会执行。保留 *args, **kwargs 转发,分别用位置和关键字形式调用,比较返回、异常、调用次数和 inspect.signature;本章已执行关键字 expected= 的成功路径。函数签名一致只证明反射结果,不代替实际调用测试。
下一章把散落的任务数据封装进最小对象,说明方法里的 self 与实例状态之间的关系。系列导航只列验收后的文章。


