同一份任务输入交给两个函数:一个把形参设为新列表,调用者看不到变化;另一个在形参上调用 append,调用者的列表却多了一个词。要判断提交的任务会处理哪份输入,不能只看变量名是否相同,得先找名称所指向的对象,再看是重新绑定了名称,还是修改了对象。

本章延续 00:从哪个 Python 开始复现 冻结的 CPython 3.12.3 和同一个 tasklab。输入仍是人工生成的文本,执行器仍是进程内顺序版本。

两个名称,一份列表

1
2
words = ["hello"]
alias = words

words 与 alias 是两个名称;alias is words 的结果为 True。不是执行了“把列表复制一份”,而是两个名称目前引用同一个列表对象。官方数据模型区分对象的身份、类型和值,还说明列表是可变对象;执行模型的名称绑定章节说明赋值如何绑定名称。这两项规则不能倒过来理解成“名称本身就是存放列表内容的盒子”。

图左侧是调用 rebind(alias) 前的关系。函数形参 incoming 起初也引用原列表;函数内 incoming = ["different"] 只让这个局部名称改指另一份列表,没有替换调用者的 words 或 alias。返回值 replacement 才引用新列表。图右侧的 append(alias) 不改变调用者的名称绑定,却修改了共同指向的列表对象,因而 words 也能观察到 "world"。

1
2
3
4
5
6
7
8
9
10
11
def rebind(incoming):
incoming = ["different"]
return incoming

def append(incoming):
incoming.append("world")

words = ["hello"]
alias = words
replacement = rebind(alias)
append(alias)

执行后 words == ["hello", "world"],replacement == ["different"]。这个反例针对“形参重新赋值会替调用者换掉实参”这一错误预测;正例是让调用方显式接收 rebind 的返回值,再按需要把自己的名称绑定到新对象。反过来,若函数承诺不改输入,仅改局部名称仍不够证明安全:函数或其他持有同一列表引用的代码仍可能调用 append。

应用在何时拿到自己的输入

本章没有把一个列表直接塞入 tasklab,而是在提交时明确把列表连接成字符串:

1
2
3
4
5
first = submit(store, "word_count", " ".join(words))
words.append("later")
second = submit(store, "word_count", " ".join(words))
run(store, first)
run(store, second)

" ".join(words) 在每次调用时创建字符串;submit 保存该字符串。第一次提交发生在列表为 ["hello", "world"] 时,第二次发生在追加 "later" 之后。再次修改外部列表不会改写之前创建的字符串。实测 first_result 是 {"hello": 1, "world": 1},second_result 是 {"hello": 1, "world": 1, "later": 1}。这里能保证的是字符串输入边界,不是“所有 Python 参数都会被自动复制”。以后任务输入包含嵌套字典时,就需要在 03 再处理浅复制与深层共享。

源码位于 examples/python-language/experiments/01_names.py,可以从工程目录运行:

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

第一条命令退出码 0,标准输出同时给出 same_list_before: true、after_rebind: ["hello"]、after_append: ["hello", "world"] 及两份结果;第二条累计运行 6 条测试,退出码 0。原始命令与输出保存在 examples/python-language/evidence/01/20261007-bindings/,不是只保存经过整理的示意值。01 的新增用例分别验证“重新绑定不换掉调用者列表”和“可变对象的修改可见,但已提交的字符串不变”;00 的四条用例仍在同一次测试中通过。

预测题

在 append(alias) 前加 alias = ["new"],之后执行 append(alias),words 会变成什么?如果写成 alias[:] = ["new"],答案一样吗?

答案:普通赋值改的是名称绑定。第一种情况下 words 仍是 ["hello"],alias 指向新列表并成为 ["new", "world"]。切片赋值修改原列表内容;第二种情况下 words 和 alias 仍指同一对象,结果都是 ["new", "world"]。这是按 3.12 语义推导的变式,尚未在本章原始运行记录里执行,不能冒称该变式已验收。

变更题

若要使第一次提交即使在以后继续追加列表、又重新绑定 alias 后也保留原词序,修改的边界应放在哪里?怎样检查?

答案:保留当前提交前的 " ".join(words),在该次调用时得到字符串,而不是让应用保留外部列表的引用;先提交任务、再改外部列表,执行前后分别查询第一个任务的 text 和结果,再提交第二个任务比较结果。现有用例验证了这一串联顺序;如果未来允许直接提交包含列表的嵌套参数,则不能照搬“字符串不可变”这一结论,必须新增隔离测试。

本章只讨论名称绑定与一层列表的修改;下一章再区分值相等、对象同一以及“空值”究竟指什么。系列导航只连接已生成的正文。

参考资料