一批三个任务都成功,结果依次是 {}、{} 和 {"word": 1}。写成 [result for result in results if result],报告里却只剩最后一项。任务并未失败,也不是推导式少遍历了两次;两个空字典是假值,被错误的筛选条件排除了。

02 已经用空文本证明空词频是合法成功结果。现在 tasklab 再收束提交、执行、查询、取消四种操作到一个分派入口,明确未知操作和无效状态的去向,而不是让布尔表达式悄悄代替业务规则。

空结果不能等于未完成

1
2
3
results = [{}, {}, {"word": 1}]
wrong = [result for result in results if result]
correct = [result for result in results if result is not None]

实测 wrong 只有 {"word": 1},correct 保留三份结果。官方布尔运算文档还指出,or 和 and 会按短路规则返回一个操作数,不总是返回布尔值。因此 result or None 同样会把合法的空字典替成 None。判断任务是否完成,应查看它的 state;判断成功任务是否有词,才查看词频字典是否为空。这两个问题不能用同一个“是否为真”的条件回答。

本章实验中,空字符串和只含空格的字符串都会成功并产生 {}。第三个任务输入 "word",得到 {"word": 1}。三项的状态都为 succeeded,而错误的真值筛选只留下一个结果。推导式并不负责定义“有效结果”,它只是照给定条件筛选。

match 是分派,不是验证器

tasklab.dispatch.dispatch 根据操作名调用已有的 submit、run、query 和 cancel,保留原业务状态规则。Python 3.12 的 match 语句为匹配到的分支执行对应代码,但不会替程序决定所有字段的合法性:

1
2
3
4
5
6
7
8
9
10
11
match operation:
case "submit":
return submit(store, payload["kind"], payload["text"], metadata=payload.get("metadata"))
case "run":
return run(store, payload["task_id"])
case "query":
return query(store, payload["task_id"])
case "cancel":
return cancel(store, payload["task_id"])
case _:
raise ValueError("unknown operation")

实际实现还在分派前检查 operation 必须为字符串,在各分支检查字段集合,拒绝多余或缺失字段;submit 内部继续检查文本和元数据。调用 dispatch(store, "submit", kind="word_count", text=None) 会匹配 submit 分支,却仍由运行输入校验抛 TypeError。未识别的 "remove" 操作抛 ValueError,任务数不变。这正是“模式匹配不自动等于类型验证”的可观察边界。

任务状态的合法集合也在应用内显式记录。若存储里的状态因错误写入变成 ghost,query 会拒绝而不是把未知状态交给上层当成功显示。当前只测试进程内字典遭到错误写入的情形,数据库事务和恢复留给 21、35,不能推测磁盘已获得同样保证。

在 examples/python-language/ 下运行:

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

第一条命令的原始输出记录 all_results 三项、lost_by_truthiness 一项、三个 succeeded 以及未知操作与错误文本对应的异常类型。两条命令退出码均为 0;累计回归达到 24 条测试,覆盖四个分派动作、空结果筛选、非法输入不污染旧任务及未知存储状态拒绝路径。环境和未整理的标准输出、标准错误位于 examples/python-language/evidence/07/20261007-dispatch/。

预测题

把正确筛选条件 result is not None 改回 if result,输入依次为 {}、{"word": 1}、None,每项会怎样?

答案:if result 只留下 {"word": 1};result is not None 保留 {} 和非空字典,排除 None。二者并不是“性能不同的写法”,而是在回答两个业务问题。本章测试执行了前者丢空结果的反例,也执行了后者保留空结果的正例。

变更题

给分派入口增加 "remove" 操作,但尚未设计状态迁移。直接增加 case "remove": store["tasks"].pop(task_id) 有哪些验收缺口?

答案:需要定义删除对 queued、running、succeeded 各状态的语义,查询和取消在删除后的返回规则、重复删除以及任务 ID 是否可复用,然后在故障路径验证没有把已提交结果和取消请求拆散。当前没有删除业务,未知操作必须被拒绝;本练习只列出新增操作前的检查条件,不把它冒充已实现功能。

下一章让 for 对任务集合请求迭代器,区分“重新遍历集合”和“已经耗尽的那一次遍历”。系列导航只列真实文章。

参考资料