结论先行:Python 既不是值传递,也不是引用传递,而是"传对象引用"(pass by object reference)。函数收到的不是实参的副本,也不是实参本身,而是同一个对象的引用——效果上类似"引用的值传递"。所以可变类型(list、dict)在函数里改内容,外面看得见;不可变类型(int、str、tuple)在函数里"改"变量,外面看不见。把这个结论背下来没用,看完下面这个实验,你一辈子忘不了。
1. 一个实验:先说反直觉的现象
def add_one(x):
x = x + 1 # 对数字"加一"
def append_one(lst):
lst.append(1) # 往列表里塞一个元素
a = 10
add_one(a)
print(a) # 10!没变
b = [1, 2]
append_one(b)
print(b) # [1, 2, 1]!变了
同样是把参数传进函数,a 纹丝不动,b 却被改了。如果 Python 是"值传递",b 不该变;如果是"引用传递",a 也该跟着变。两个都不符合——所以这个问题才吵了这么多年。
别急,先记住这个现象:变的是 list,不变的是 int。规律就藏在类型里。
2. 本质:Python 的变量不是"盒子",是"标签"
理解这一切,只需要接受一个设定:
Python 的变量名不存值,它只是一个贴在对象上的标签(名字绑定)。
a = 10
这行代码的底层动作是:右边先造出一个整数对象 10,然后让名字 a 指向它。a 本身不占地方存"10",它只是个引用,相当于便利贴。
赋值语句 c = a 更不是"复制了一个盒子"——它只是把新标签 c 也贴到同一个对象上:
a = [1, 2, 3]
c = a
c.append(4)
print(a) # [1, 2, 3, 4]——a 和 c 贴的是同一个对象
这就是为什么 is 和 == 必须分开:== 比的是对象里的值,is 比的是"是不是同一个对象"。c is a 在这里是 True。
关键认知:变量是标签,那"改变量"就有两种完全不同的含义——把标签撕下来贴到别的对象上(重新绑定),还是钻进对象里面改内容。这两件事,对可变/不可变类型的效果天差地别。
3. 传参:传的是"标签的复印件"
调用函数时发生的事,可以用一句话说清:
函数收到的是实参引用的副本——一个新标签,贴到同一个对象上。
def append_one(lst): # lst 是新标签,和 b 贴同一个列表
lst.append(1) # 钻进对象改内容 → b 看得见
def add_one(x): # x 是新标签,和 a 贴同一个 10
x = x + 1 # 撕下 x,贴到一个新对象 11 上 → a 无感
现在回头看第 1 节的两个现象,全部解释通了:
| 函数里的动作 | 实际发生了什么 | 外部看得见吗 |
|---|---|---|
lst.append(1) |
通过标签钻进对象,改内容 | 看得见(对象被改了) |
x = x + 1 |
重新绑定:x 指向新对象 |
看不见(a 还贴着老对象) |
注意一个细节:x = x + 1 里的 + 1 其实也没"修改"整数对象——整数是不可变的,+ 只会造出一个新对象。所以严格说,不可变类型在函数里连"改内容"这个操作都不存在,你只能换绑。
4. mutable vs immutable:一张表说清
| 类型 | 常见成员 | 能"钻进对象改内容"吗 | 传参后的表现 |
|---|---|---|---|
| 不可变 | int、float、str、tuple、frozenset | 不能 | 函数内重新赋值,外部无感 |
| 可变 | list、dict、set、bytearray | 能 | 函数内改内容,外部同步看见 |
这张表就是全部规则。所谓"Python 是值传递还是引用传递"的争论,八成是把这两种表现混为一谈了。
再加一层保险:即使传进去的是可变对象,只要你在函数里写 lst = [新列表],那也是重新绑定——外部照样看不见。判定标准永远是那句:这行代码是"改内容"还是"换标签"。
5. 经典陷阱:默认参数为什么"记仇"
def bug(item, target=[]):
target.append(item)
return target
print(bug(1)) # [1]
print(bug(2)) # [1, 2]?!说好的默认空列表呢
陷阱根源:默认参数只在函数定义时求值一次,那个 [] 从始至终是同一个对象——所有没传 target 的调用,都在往同一个列表里塞东西。
正确写法:用 None 当哨兵,函数体内再创建:
def fixed(item, target=None):
if target is None:
target = []
target.append(item)
return target
这个坑能坑住人,靠的正是"可变对象 + 引用共享"两个知识点叠加。反过来,不可变类型做默认参数(如 def f(n=0))就安全——反正没人能改它。
6. 同类陷阱:+= 不总是等于 +
+= 对可变类型是"就地修改"(调用 __iadd__),对不可变类型是"重新绑定":
a = [1, 2]
b = a
a += [3] # 就地改:a 和 b 还是同一个对象
print(b) # [1, 2, 3]
x = 1
y = x
x += 1 # 重新绑定:y 无感
print(y) # 1
同样一个 +=,行为分裂成两半。面试官爱问这个,不是爱抠字眼,是在考察你有没有"可变/不可变"这根弦。
7. 一句话总结
- 变量是贴在对象上的标签,赋值是贴新标签,不是复制内容;
- 传参传的是引用的副本——新标签贴老对象;
- 能不能影响外部,只看一件事:改内容(可变)还是换标签(重新绑定);
- 所以 Python 的准确叫法是 传对象引用(pass by object reference)。
把这四句话想明白,比背"值传递/引用传递"任何一个结论都管用。
下一篇:《Python 的垃圾回收到底回收了谁?引用计数+分代回收一次讲清》——一个 del 之后,对象在内存里经历了什么。关注不迷路。