本节摘要:Python 的赋值语句不拷贝值,只把名字绑定到对象;传参也是如此。本节用一组可复现实验建立"名字—对象"模型,解释别名现象、可变默认参数坑、以及 Python 既不是值传递也不是引用传递的准确说法——对象引用传参。
下面这段模式在真实代码库里出现过无数次:
def add_tag(tags=[]): # 危险!默认值在函数定义时求值一次 tags.append("vip") return tags print(add_tag()) # ['vip'] print(add_tag()) # ['vip', 'vip'] ← 惊讶吗? print(add_tag()) # ['vip', 'vip', 'vip']
按"每次调用重新执行函数"的直觉,三次都该返回 ['vip']。但实际每次都多一个。原因藏在两件机制的组合里:
[] 在 def 语句执行时求值一次,之后一直复用同一个 list 对象;append 修改的是那个对象本身,而每次调用用的都是它。两个机制都指向同一个根:Python 里名字与对象是分离的,语句操作的是名字到对象的绑定。函数甚至保留了那个默认对象的证据:
>>> add_tag.__defaults__ (['vip', 'vip', 'vip'],)
正确写法是用 None 哨兵:
def add_tag(tags=None): if tags is None: tags = [] # 每次调用新建 tags.append("vip") return tags
x = 1000 的完整语义:在当前命名空间创建(或更新)名字 x,让它指向一个 int 对象。没有盒子,没有拷贝,只是"贴标签"。
由此推演出赋值的全部行为:
>>> a = [1, 2, 3] >>> b = a # b 与 a 指向同一对象 >>> b is a True >>> b.append(4) # 通过 b 改对象 >>> a # a 名下当然也变了——它们本来就是同一个对象 [1, 2, 3, 4]

什么时候才发生真正的拷贝?只有你显式要求:list(a)、a.copy()、a[:],或标准库的 copy.deepcopy。注意浅拷贝只复制最外层容器,内层子对象仍共享——嵌套结构的"部分别名"是又一类隐蔽 bug:
>>> grid = [[0, 0], [0, 0]] >>> shallow = grid.copy() >>> shallow[0].append(9) >>> grid # 内层列表被共享,grid 也遭殃 [[0, 0, 9], [0, 0]]
还有一个同源的经典翻车:用乘法快速建二维表格:
>>> board = [[0] * 3] * 3 # 三行其实是同一行! >>> board[0][0] = 1 >>> board [[1, 0, 0], [1, 0, 0], [1, 0, 0]] >>> board_ok = [[0] * 3 for _ in range(3)] # 推导式每行新建,正确
把"赋值是绑定"延伸到函数调用,规则只有一条:实参对象引用绑定给形参名字。函数内对形参重新赋值,只是给标签换地方,不影响外部;对对象做原地修改,外部立刻可见:
def rebind(n): n = [9, 9] # 形参换了标签,调用者的 list 无感 def mutate(n): n.append(9) # 通过标签改对象,调用者看得见 data = [1] rebind(data); print(data) # [1] mutate(data); print(data) # [1, 9]
所以"Python 是值传递还是引用传递"这场经典争论的答案是:两者都不准确。参数传的是对象引用,且赋值语义像"值"(换标签互不影响)、修改语义像"引用"(动对象全局可见)。理解绑定,争论自动消解。
用 dis 亲眼看一次赋值的字节码,验证"绑定"的说法:
>>> import dis >>> dis.dis("a = b") 0 RESUME 0 2 LOAD_NAME 1 (b) # 找到 b 指向的对象 4 STORE_NAME 0 (a) # 把名字 a 绑到该对象
STORE_NAME——存储的是名字,不是值。这就是"名字绑定"在解释器层面的字面证据。
x += y 对不可变对象等价于 x = x + y(造新对象、换标签);对可变对象则可能原地执行(list 的 += 走 __iadd__,直接 extend):
>>> lst = [1, 2] >>> alias = lst >>> lst += [3] # 原地扩展 >>> alias # alias 看得见 [1, 2, 3] >>> t = (1, 2) >>> at = t >>> t += (3,) # tuple 不可变,造新对象 >>> at # at 还停在旧元组 (1, 2)
同一行 +=,可变性不同、后果完全不同。这类"可变性决定语义"的现象在第 2 章五大容器里会反复出现,值得现在就建立敏感度。
==,判断 None 用 is,其余场景禁用 is;💡 关键直觉:读 Python 代码时,把每个等号翻译成"让这个名字指向右边求值得到的对象"。坚持这个翻译习惯两周,别名类 bug 在你手里会绝迹。
STORE_NAME 存名字不存值;拷贝只在你显式要求时发生。deepcopy 才彻底隔离。+= 依可变性分叉:list 原地扩展,tuple 造新对象。第 2 章把这套模型应用到五大内置容器上:为什么 list 是 list、dict 为什么快、字符串为什么不可变——全部能从对象模型推出。