词频结果 {"one": 2, "two": 1} 可以回答两个不同的长度问题:有多少种词,和总共出现多少次词。让 len(result) 返回 2、result.total 返回 3,就必须把各自的协议含义说清楚;仅因为都叫“长度”,不能把计数之和塞进 __len__ 而不告知调用方。

13 的数据类现在代表了词频结果,业务状态仍由 Task 持有。本章只给已有结果加最小 __len__ 和受控的只读属性;没有通过一套神奇运算符隐藏任务提交、取消和查询。

len 会找哪个方法

1
2
3
4
5
6
7
8
9
10
@dataclass
class TaskResult:
counts: dict[str, int] = field(default_factory=dict)

def __len__(self):
return len(self.counts)

@property
def total(self):
return sum(self.counts.values())

实测 len(result) 与 TaskResult.__len__(result) 都返回 2。前者走语言的特殊方法协议,后者是显式调用类上定义的函数;相同的返回值支持当前实现的语义,却不意味着 Python 一律按普通实例属性查找特殊方法。将 result.__len__ 在实例上临时改成返回 99 后,result.__len__() 为 99,len(result) 仍为 1(该测试用的是只有一种词的另一实例)。Python 3.12 数据模型的特殊方法查找明确描述了从类型查找所带来的差异。

total 是 property,按现有计数字典实时求和,不存一份容易与 counts 不一致的冗余总数。property 文档给出了描述符的访问协议。词频结果内仍有可变字典,因此在业务边界由 Task.snapshot 复制结果;只读属性不是“整个对象不可变”的证明。

状态属性只能守住约定的入口

任务状态属性 task.state 由 property 校验合法词表:queued、running、succeeded、failed、cancel_requested、cancelled。尝试 task.state = "ghost" 得到 ValueError,原本成功的任务经 query 仍为 succeeded。业务动作上的迁移约束继续由 run、cancel 执行;仅接受合法状态名,并不能阻止内部代码绕过业务动作写入另一合法但不应出现的终态。

这也不是安全沙箱。内部 _state 能被故意错误写入,07 保留了直接污染内部字段后由快照拒绝 ghost 的故障断言;调用方通过正常入口不应持有内部 Task。描述符可帮助收紧常规属性访问,却不能代替事务、并发同步或隔离不可信输入。

从 examples/python-language/ 执行:

1
2
PYTHONPATH=src /usr/bin/python3.12 experiments/14_protocols.py
PYTHONPATH=src /usr/bin/python3.12 -m unittest discover -s tests -v

两条命令均以 0 退出,累计 43 条测试。演示输出 distinct_words: 2、total_words: 3、invalid_error: "ValueError"、state_after_rejection: "succeeded",新增断言还比较直接调用与协议调用以及实例遮蔽反例。输入环境、原始输出、退出码在 examples/python-language/evidence/14/20261007-protocol/;repr 的实测字符串也在输出中,不把 repr 当作稳定 JSON 序列化格式。

预测题

空的 TaskResult() 上,len(result) 和 result.total 分别是什么?可以凭 not result 判定任务未完成吗?

答案:两者均为零。定义 __len__ 后,空结果在未另行定义 __bool__ 时可以是假值,但任务可能已经 succeeded;必须查任务状态,不能用结果真值代替完成判断。02、07 的空结果用例已验证这种业务分界。

变更题

如果内部代码误把成功任务的 _state 直接改成 ghost,仅靠 state 的 setter 能发现吗?需要在哪条路径核对?

答案:直接写 _state 绕过属性 setter;Task.snapshot 再检查状态词表会拒绝,query 因此抛 ValueError。该故障注入路径由 test_unknown_stored_state_is_not_silently_returned 实际执行,但不能推出存储引擎或任意并发写入已被保护。

下一章要在异常传播的具体层次决定何时标记 failed、何时保留调用栈与原因。系列导航只链接已验收章节。

参考资料