两个任务的标签本应独立:第一次提交 "first",第二次提交 "second"。把默认参数写成 labels=[],第二个任务却得到 ["first", "second"]。03 明明已经在 submit 里复制了列表,为什么仍会串进前次的标签?因为污染发生在调用 submit 之前:包装函数两次使用了同一份默认列表。

tasklab 的公开入口仍是 submit(store, kind, text, *, metadata=None)。本章不重写任务应用,而在它前面增加一层故意写错和一层修好的提交函数,比较同一业务动作的输入。

默认值不是每次调用时重新求值

1
2
3
def unsafe_submit(store, label, labels=[]):
labels.append(label)
return submit(store, "word_count", label, metadata={"labels": labels})

Python 3.12 教程的默认参数章节说明,默认值在函数定义执行时求值一次。函数对象随后持有那份列表;调用一次就在其中追加一次。tasklab.submit 复制当前收到的标签,从而保证已经提交的第一个任务仍保留 ["first"];但包装函数的默认列表已经被改变,第二次提交收到的就是 ["first", "second"]。从“已保存任务没有被反向改写”,推不出“下一次提交不会被污染”。

对应的边界是用不可变的标记表示“省略了标签”,每次调用自行建列表。如果显式传进了调用者的列表,也要先按本业务字段模式复制,免得 append 修改调用方的数据:

1
2
3
4
def safe_submit(store, label, *, labels=None):
selected = [] if labels is None else labels.copy()
selected.append(label)
return submit(store, "word_count", label, metadata={"labels": selected})

修复不是“到处把可变对象改成 None”那么简单;只有声明了“None 表示没有提供”的接口才可以这样写。本章 submit 本身在任务创建前校验 metadata,并对受支持的 labels 列表做入库前和查询后的隔离。safe_submit 新建列表是修复其自身的跨调用污染,两个层次缺一不可。

* 把错误留在任务创建之前

公开接口中的 * 要求 metadata 只能以关键字指定,不能当成第四个位置参数。官方关键字限定参数文档给出此语法的调用规则:

1
submit(store, "word_count", "ok", metadata={"labels": []})

上述调用有效;把 metadata= 去掉、或者干脆省略必需的 text,都会在进入函数主体前得到 TypeError,不分配任务 ID。任务种类不支持、文本类型不对等失败则发生在 submit 主体的输入校验中,错误类型和位置不同。签名约束并不能替代正文里的数据类型检查,后续还有不可信 JSON 输入需要处理。

同一入口的正反例

在 examples/python-language/ 执行:

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

第一条命令退出码 0,原始输出给出 unsafe_first: ["first"]、unsafe_second: ["first", "second"];安全包装分别得到 ["third"] 和 ["fourth"]。无效调用报告 TypeError,任务数保持不变。第二条命令运行累计 18 条测试,退出码 0,新增断言还检查显式传给安全包装的调用者列表不被修改。环境、命令、输出和断言在 examples/python-language/evidence/05/20261007-defaults/。

预测题

对 unsafe_submit 连续调用三次,标签分别是 "a"、"b"、"c",已经保存的第一项会变成三个标签吗?第三项会拿到什么?

答案:由于 03 的提交隔离,第一项仍是 ["a"];第三次包装函数会看到跨调用累积的默认列表,第三项成为 ["a", "b", "c"]。本章运行了两次污染调用;三次是按同一规则推导的练习,尚未另行执行。

变更题

把安全包装里的 labels.copy() 删掉,只保留 selected = [] if labels is None else labels。不省略参数、显式传入 caller = ["kept"] 时需要新增什么断言?

答案:selected.append(label) 将直接修改 caller,即使 submit 自己会复制入库,也不能保护调用方。应在调用前后比较 caller 的内容,并分别验证已提交任务的标签;现有 test_none_default_creates_fresh_list_and_copies_caller 会抓到该回归。错误改动未应用到正式函数。

下一章转向闭包:默认参数不仅能作为调用约束,还能在回调创建时固定循环变量的值。系列导航仅链接已落盘正文。

参考资料