把任务元数据 {"labels": ["short"]} 复制一份,再向复制品里的 labels 追加 "shared"。如果两份元数据都变了,问题不在词频计算,而在复制发生的层级:字典的外壳换了,里面那份列表没有换。01 的名称与对象引用图在这里多出一层;只看 metadata is not shallow 会漏掉真正的共享点。

前两章已固定 输入的名称绑定和空文本的合法性。现在 tasklab 的 submit 增加一个可选元数据字段,仍只接受白名单任务和人工生成的字符串,不引入第二套工程。

新字典,旧列表

1
2
3
metadata = {"labels": ["short"]}
shallow = metadata.copy()
shallow["labels"].append("shared")

执行后 metadata["labels"] == ["short", "shared"],而 shallow["labels"] is metadata["labels"] 为 True。Python 3.12 copy 模块文档明确区分浅复制与深复制:前者建立新的复合对象,却保留内部对象的引用;后者递归复制内部对象,但其行为也要面对循环和不应复制的外部资源。当前输入模式仅有字符串标签列表,没理由把任意对象的递归复制当作普遍正确的边界。

元数据是一个明确的小模式:只允许 {"labels": [字符串, ...]}。submit 在创建任务 ID 之前检查这个形状,把标签列表复制进新字典;查询时也返回独立的标签列表。这样外部调用者修改原列表,或修改查询返回的快照,都不能改写库内状态。修复要覆盖两个方向——提交入口与查询出口,不能只在前者调用 .copy()。

1
2
3
4
first = submit(store, "word_count", "hello", metadata=metadata)
metadata["labels"].append("later")
second = submit(store, "word_count", "world", metadata=metadata)
query(store, first)["metadata"]["labels"].append("outside")

实际查询第一个任务仍得到 ["short", "shared"],第二个得到 ["short", "shared", "later"]。"outside" 只存在于调用方的查询结果;两次词频结果分别为 {"hello": 1} 与 {"world": 1}。这是在有限输入模式下的隔离断言,不证明任意嵌套对象都能复制或任意回调都没有副作用。传入 {"labels": "bad"}、{"labels": [1]} 或未知字段会拒绝,任务数和下一 ID 保持不变。

为什么元数据字段不能随便拿去当键

字典键依赖相等和哈希的一致性。两份分别构造的元组 ("label", "short") 和 tuple(["label", "short"]) 相等、哈希也相等;以第一份为键写入 "known",用第二份查询仍能得到 "known"。把可变的标签列表直接当键会抛 TypeError。Python 数据模型的哈希说明要求参与比较的对象遵守相等与哈希合同;可变列表不能安全承担此角色。

不要从“元组可哈希”推出“任何装着可变对象的元组都可安全作为键”。若元组中包含不可哈希的列表,求哈希同样会失败。tasklab 只用任务 ID 字符串定位任务,不把元数据标签当成可变键。

正反例与累计回归

在 examples/python-language/ 下执行:

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

两条命令退出码均为 0。第一条的原始 JSON 包含 "shallow_shared": true、隔离后的两组标签和 "lookup": "known";第二条累计运行 12 条测试,覆盖浅复制反例、提交及查询隔离、非法元数据不占 ID、相等键检索,还继续运行 00–02 的断言。examples/python-language/evidence/03/20261007-copies/ 保存环境、原始标准输出/错误、命令与断言。这里没测到深复制任意业务对象的成本,不能凭一个小字典宣称所有输入都应使用 deepcopy。

预测题

若 submit 只执行 metadata.copy(),却不复制 metadata["labels"],提交后调用者追加标签,会看到怎样的结果?

答案:任务中的新外层字典仍引用旧标签列表;追加会被后续 query 观察到。判断依据是内层引用是否相同,而不是外层字典是否为新对象。test_shallow_copy_leaks_nested_mutation 在同样的嵌套结构上执行了这一反例。

变更题

把 labels 里的字符串改成列表,例如 [["short"]],目前的隔离测试还能说明提交安全么?

答案:不能。当前模式会在提交前拒绝这种输入,因此没有“被复制后安全”的证明。如果业务必须接收嵌套列表,需要先定义字段模式、所有权和查询快照,再加入修改最内层列表的正反例;仅在旧用例上追加一次外层 .copy() 没有覆盖新风险。该变更未实施,验收状态仍为 NOT_RUN。

下一章将字符串输入推到字节边界,记录 UTF-8 解码失败时错误属于哪一层。系列导航只连接已完成的正文。

参考资料