Python 中变量、对象和引用是什么关系?
简化版
Python 里的变量不是盒子,而是名字;对象才是真正存放数据的实体,变量只是绑定到对象的引用。执行 b = a 时,不会复制对象,只是让 b 这个名字也指向 a 指向的同一个对象。
详细版
Python 的赋值语义可以理解为“名字绑定对象”:
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4]
这里 a 和 b 指向同一个列表对象,所以通过 b 修改列表,a 看到的也是修改后的结果。
但如果重新绑定变量:
a = [1, 2, 3]
b = a
b = [9, 9, 9]
print(a) # [1, 2, 3]
b = [9, 9, 9] 是让 b 指向新列表,不会影响 a 原来指向的列表。
面试里要讲清三个词:
- 对象:内存里的数据实体,例如列表、字符串、整数、字典。
- 变量:对象的名字,不是对象本身。
- 引用:变量和对象之间的指向关系。
这套模型能解释很多 Python 易错点:可变对象共享、默认参数坑、浅拷贝、函数传参效果等。
完整版教学
一、变量不是盒子,而是名字绑定
很多初学者会把变量想成盒子:
a 盒子里装着 [1, 2, 3]
这个模型在 Python 里容易误导。更准确的模型是:
[1, 2, 3] 是对象
a 是贴在对象上的标签
赋值语句:
a = [1, 2, 3]
意思不是“把列表放进 a 这个盒子”,而是“创建一个列表对象,然后让名字 a 绑定到它”。
a = [1, 2, 3]
名字表: a ──────> 列表对象 [1, 2, 3]
这个模型能解释 Python 里大量现象:赋值不复制对象,函数传参不复制对象,浅拷贝只复制外层容器,可变对象原地修改会被多个名字看到。面试时只要先把“名字、引用、对象”三层拆开,后面很多追问都顺了。
二、多个变量可以指向同一个对象
看这个例子:
a = {"name": "Tom"}
b = a
b["age"] = 18
print(a) # {'name': 'Tom', 'age': 18}
a 和 b 不是两个字典,它们是同一个字典对象的两个名字。只要对象本身被修改,所有指向它的变量都会看到变化。
可以用 id() 粗略观察对象身份:
print(id(a) == id(b)) # True
id() 返回对象身份标识。在 CPython 中常表现为内存地址相关的值,但面试里说“对象身份标识”更准确,不要把它绝对等同于语言层面的内存地址。
数字上可以这样理解:一开始有 1 个字典对象、2 个名字指向它;b["age"] = 18 没有创建第二个字典,只是在原字典里多放了一个键值对。因此从 a 或 b 看过去,看到的都是同一个被修改后的对象。
三、修改对象和重新绑定变量是两回事
这两个操作特别容易混:
a = [1, 2, 3]
b = a
b.append(4) # 修改对象
b = [9, 9, 9] # 重新绑定变量
append 是对原列表对象做原地修改,所以 a 能看到变化;b = [9, 9, 9] 是让 b 指向新对象,不会动原列表。
可以画成:
修改前:a ─┐
├──> [1, 2, 3]
b ─┘
b 重新绑定后:
a ───> [1, 2, 3]
b ───> [9, 9, 9]
对可变对象,append、extend、dict.__setitem__ 这类操作会改变对象内容;赋值语句则改变名字绑定。这个区别不是语法小细节,而是判断副作用边界的核心。
| 代码 | 做了什么 | a 是否受影响 |
|---|---|---|
b = a | 让 b 绑定同一对象 | 共享对象 |
b.append(4) | 修改共享列表对象 | 是 |
b = [9, 9, 9] | b 重新绑定新对象 | 否 |
a = a + [4] | 创建新列表并让 a 指向它 | b 不跟着变 |
四、函数传参也是名字绑定
Python 函数传参常被描述成“传对象引用”。更准确地说,是把实参对象绑定到形参名字上。
def add_item(items):
items.append("new")
data = []
add_item(data)
print(data) # ['new']
函数内部的 items 和外部的 data 指向同一个列表对象,所以原地修改会影响外部。
但重新绑定形参不会影响外部变量:
def reset(items):
items = []
data = [1, 2]
reset(data)
print(data) # [1, 2]
items = [] 只是让局部名字 items 指向新列表,不会改变 data 的绑定。
流程图如下:
调用 add_item(data)
外部 data ─┐
├──> []
形参 items ┘
items.append("new") 修改同一个列表,所以 data 能看到
这也解释了为什么 Python 里常说“既不是传统值传递,也不是 C++ 那种引用传递”,更稳的说法是对象共享调用或赋值绑定调用:形参是新名字,绑定到实参对象;修改对象可见,重新绑定形参不可见。
五、可变对象和不可变对象的表现差异
同样是 +=,可变对象和不可变对象可能表现不同:
a = [1, 2]
b = a
a += [3]
print(b) # [1, 2, 3]
x = "ab"
y = x
x += "c"
print(y) # "ab"
列表的 += 通常走原地扩展,a 和 b 共享的列表对象被修改;字符串不可变,x += "c" 创建新字符串并让 x 绑定过去,y 仍指向老字符串。这里不是变量规则变了,而是对象类型的可变性不同。
六、这套模型能解释哪些高频问题
理解“名字绑定对象”之后,很多题都会变简单:
a is b判断两个名字是否指向同一对象。a == b判断两个对象的值是否相等。- 可变对象原地修改会影响共享引用。
copy.copy()只复制外层容器,内部对象仍可能共享。- 默认参数在函数定义时创建,所以可变默认值会被多次调用共享。
- 不可变对象不能原地修改,所谓“修改字符串”本质是创建新字符串并重新绑定变量。
记忆钩子:赋值语句改的是“名字指向谁”,方法调用常常改的是“对象里面有什么”;先分清这两件事,再判断外部能不能看到变化。
七、常见误区与追问
- 误区:
b = a会复制一份对象。 它只增加一个名字绑定,除非显式 copy,否则还是同一个对象。 - 误区:函数里给形参重新赋值能改变外部变量。 形参是局部名字,重新绑定只影响局部名字,不影响调用方名字。
- 误区:可变对象传参和不可变对象传参规则不同。 传参都是名字绑定到对象,差异来自对象能否原地修改。
- 追问:
id()是不是内存地址? 语言层面应说对象身份标识;CPython 中常与内存地址相关,但不应泛化到所有实现。 - 追问:为什么列表
+=会影响别名而字符串不会? 列表可原地扩展,字符串不可变只能创建新对象再绑定。 - 追问:如何避免共享引用带来的联动修改? 需要显式拷贝,嵌套可变结构还要考虑深拷贝或按需重建。
八、加强记忆
把 Python 赋值记成“名字贴标签”:对象是内存中的实体,变量是贴在对象上的名字,引用是名字到对象的指向关系。b = a 是多贴一个标签,不是复制对象;原地修改会被所有标签看到,重新绑定只把当前标签贴到别的对象上。这个模型能贯穿 is/==、可变性、函数传参、默认参数和拷贝问题。