Python 05:函数签名如何表达调用约束
两个任务的标签本应独立:第一次提交 "first",第二次提交 "second"。把默认参数写成 labels=[],第二个任务却得到 ["first", "second"]。03 明明已经在 submit 里复制了列表,为什么仍会串进前次的标签?因为污染发生在调用 submit 之前:包装函数两次使用了同一份默认列表。
tasklab 的公开入口仍是 submit(store, kind, text, *, metadata=None)。本章不重写任务应用,而在它前面增加一层故意写错和一层修好的提交函数,比较同一业务动作的输入。
默认值不是每次调用时重新求值
1 | |
Python 3.12 教程的默认参数章节说明,默认值在函数定义执行时求值一次。函数对象随后持有那份列表;调用一次就在其中追加一次。tasklab.submit 复制当前收到的标签,从而保证已经提交的第一个任务仍保留 ["first"];但包装函数的默认列表已经被改变,第二次提交收到的就是 ["first", "second"]。从“已保存任务没有被反向改写”,推不出“下一次提交不会被污染”。
对应的边界是用不可变的标记表示“省略了标签”,每次调用自行建列表。如果显式传进了调用者的列表,也要先按本业务字段模式复制,免得 append 修改调用方的数据:
1 | |
修复不是“到处把可变对象改成 None”那么简单;只有声明了“None 表示没有提供”的接口才可以这样写。本章 submit 本身在任务创建前校验 metadata,并对受支持的 labels 列表做入库前和查询后的隔离。safe_submit 新建列表是修复其自身的跨调用污染,两个层次缺一不可。
* 把错误留在任务创建之前
公开接口中的 * 要求 metadata 只能以关键字指定,不能当成第四个位置参数。官方关键字限定参数文档给出此语法的调用规则:
1 | |
上述调用有效;把 metadata= 去掉、或者干脆省略必需的 text,都会在进入函数主体前得到 TypeError,不分配任务 ID。任务种类不支持、文本类型不对等失败则发生在 submit 主体的输入校验中,错误类型和位置不同。签名约束并不能替代正文里的数据类型检查,后续还有不可信 JSON 输入需要处理。
同一入口的正反例
在 examples/python-language/ 执行:
1 | |
第一条命令退出码 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 会抓到该回归。错误改动未应用到正式函数。
下一章转向闭包:默认参数不仅能作为调用约束,还能在回调创建时固定循环变量的值。系列导航仅链接已落盘正文。






