Python 12:方法调用为何会多出 self
同一进程里,第一个任务已经成功,第二个还在队列中。若把任务状态放在类级别,两项就可能意外共用;若每项拥有自己的实例,执行第一项不应改动第二项。self 不是调用方手动输入的额外任务 ID,而是经实例访问函数时被绑定的那个实例。
前面几章的业务任务暂存在字典中,10 的分批词频没有改变状态管理。本章将任务记录提炼为内部 Task 类,公开的 submit/run/query/cancel 及查询快照仍维持原来的业务接口。
数据从公共字典进入每个实例
Task.__init__(self, kind, text, metadata) 给各实例建立独立的 state、result 和标签列表;Task.snapshot(self) 返回给调用方仍是字典。实际创建两项后,第一项 run 到 succeeded,第二项还是 queued;第一项标签为 ["first"],第二项是 ["second"]。这既检查类的实现,也检查替换内部表示后旧调用约定没有变。
Python 3.12 教程的方法对象一节区分了类里的函数与经实例访问得到的绑定方法。对第一项任务,实测 task.snapshot.__self__ is task,task.snapshot.__func__ is Task.snapshot;task.snapshot() 与 Task.snapshot(task) 产生相等快照。前一种调用由绑定方法传入实例,后一种写法显式指定,不存在运行时凭名称猜一个 self。
类级列表为何让两个任务串改
1 | |
两个实例开始都没有自己的 labels 属性,属性查找回到类级同一份列表;第一次追加后第二项也看到 ["leak"]。这与 03 的嵌套浅复制是两种不同的共享入口:一个来自类属性查找,另一个来自外层容器复制后保留的内层引用。官方类变量与实例变量文档专门给出了可变类变量被所有实例意外共享的反例。
生产 Task 没有把标签列表放在类级别,而是在实例初始化时按已验证模式复制。query 仍生成快照并复制列表和结果,调用方不能通过返回值改写存储中的另一个任务。13 会加入 dataclass,再检查自动生成方法和 frozen=True 究竟约束到哪一层。
在 examples/python-language/ 执行:
1 | |
两条命令以 0 退出,累计 37 条测试。演示输出包含两个不同终态、绑定方法的两个身份判断和类级列表的泄漏;此章修改内部存储后仍重放 00–11 的断言。环境、原始 stdout/stderr 与结果记录在 examples/python-language/evidence/12/20261007-methods/。Task 的字段此时仍可被内部代码重新赋值,不该把“实例数据独立”说成“对象深度不可变”。
预测题
对两份不同 Task 实例依次执行 Task.snapshot(first) 与 second.snapshot(),哪一个返回的任务状态取决于 first?如果 first 已成功,第二项仍排队,结果应怎样?
答案:显式传入 first 的前者返回成功快照;经 second 访问绑定方法的后者返回排队快照。两种调用形式在实验中核对过同一实例的相等结果,不是说“所有实例都返回同一状态”。
变更题
将 Task.__init__ 中的 self.metadata = ... 删除,改成类属性 metadata = {"labels": []},测试需要怎样抓住共享问题?
答案:创建两份实例,给第一份追加标签并查询第二份,第二份仍应保留独立标签;若改动造成共享,此断言失败。还要保证 03 的提交及查询复制边界没被破坏。本章的 BrokenTask 已执行类级共享反例,但没有将它作为正式 Task 的实现。
下一章把任务定义提炼成 dataclass,核对字段默认工厂、相等和冻结的边界。系列导航只列实际完成章节。


