Python 中可变对象和不可变对象有什么区别?
简化版
可变对象创建后内容还能原地修改,比如 list、dict、set;不可变对象创建后值不能原地改变,比如 int、float、str、tuple。区别的关键不是变量能不能重新赋值,而是对象本身能不能被原地修改。
详细版
常见分类:
| 类型 | 是否可变 | 示例 |
|---|---|---|
int、float、bool | 不可变 | x = 1 |
str | 不可变 | "hello" |
tuple | 通常视为不可变容器 | (1, 2) |
list | 可变 | [1, 2] |
dict | 可变 | {"a": 1} |
set | 可变 | {1, 2} |
示例:
s = "abc"
s += "d" # 创建新字符串,再让 s 绑定过去
nums = [1, 2]
nums.append(3) # 原地修改同一个列表对象
注意:不可变不是“变量不能变”。变量随时可以重新绑定:
x = 1
x = 2
这里不是把整数 1 改成 2,而是让 x 从整数对象 1 改为指向整数对象 2。
完整版教学
一、怎么判断“可变”
判断一个类型是否可变,看它是否支持在对象身份不变的情况下修改内容。
列表可以:
a = [1, 2]
before = id(a)
a.append(3)
after = id(a)
print(before == after) # True
print(a) # [1, 2, 3]
字符串不可以:
s = "ab"
before = id(s)
s += "c"
after = id(s)
print(before == after) # 通常是 False
print(s) # abc
s += "c" 看起来像修改,其实是创建新字符串,再让 s 绑定到新对象。注意 id() 只是观察对象身份的工具,某些解释器优化可能复用对象,所以面试里不要把单次 id() 结果当成唯一证据;真正的判断标准是类型是否提供原地改变对象内容的语义。
list.append:
nums ───> [1, 2] 原对象变成 [1, 2, 3]
str +=:
s ───> "ab" 新建 "abc",再让 s 指过去
二、变量重新赋值不等于对象可变
很多人把“变量可以变”和“对象可变”混在一起。Python 变量只是名字,名字当然可以重新绑定到别的对象;可变性讨论的是原对象本身能不能被改。x = 1; x = 2 不是把整数对象 1 改成 2,而是让名字 x 从一个对象指向另一个对象。
x = 100
old = id(x)
x = x + 1
new = id(x)
print(old == new) # 不应期待为 True
对不可变对象执行“看起来像修改”的操作,通常会得到新对象。对可变对象执行原地方法,才会让共享引用都看到变化。这个区别会影响函数传参、默认参数和拷贝行为。
三、tuple 为什么说“通常视为不可变”
tuple 本身不可变,不能增删元素,也不能替换某个位置:
t = (1, 2)
# t[0] = 9 # TypeError
但如果 tuple 里面放的是可变对象,这个内部对象仍然可以变:
t = ([1, 2], "a")
t[0].append(3)
print(t) # ([1, 2, 3], 'a')
这不是 tuple 自己变了,而是 tuple 持有的列表对象内容变了。面试里说“tuple 不可变”时,最好补一句:不可变的是 tuple 的元素引用关系,不代表其中引用的可变对象内容不能变。也正因为这个原因,包含 list 的 tuple 不能作为 dict key。
ok = {(1, 2): "point"}
# bad = {([1, 2], 3): "value"} # TypeError: unhashable type: 'list'
四、可变性会影响函数传参
可变对象传入函数后,函数内部原地修改会影响外部:
def add(nums):
nums.append(3)
x = [1, 2]
add(x)
print(x) # [1, 2, 3]
不可变对象不能原地改,函数内部通常只能重新绑定局部变量:
def inc(n):
n += 1
x = 1
inc(x)
print(x) # 1
n += 1 创建了新整数对象并绑定到局部变量 n,不会改变外部的 x。这里不是 Python 对可变对象和不可变对象使用了两套传参规则,而是“共享对象能否被原地改”的结果不同。Python 传参可以理解为把实参对象绑定给形参名字,形参重新绑定不影响外部名字。
易错点:可变性看的是对象能不能原地改,不是变量名能不能改指向;
x = 新对象永远只是重新绑定名字。
| 传入对象 | 函数内操作 | 外部是否可见 |
|---|---|---|
list | append 原地修改 | 可见 |
list | nums = [] 重新绑定 | 不可见 |
int | n += 1 新建并绑定 | 不可见 |
dict | d["x"] = 1 原地修改 | 可见 |
五、可变性会影响哈希和字典 key
字典 key 和集合元素需要可哈希。可哈希对象的哈希值在生命周期内必须稳定,所以大多数可变对象不能作为 dict key。
d = {}
# d[[1, 2]] = "value" # TypeError: unhashable type: 'list'
d[(1, 2)] = "value" # 可以
如果允许列表作为 key,列表内容改变后哈希位置也可能变化,字典就无法可靠找到它。可以用一个数字例子理解:假设某 key 根据内容 [1, 2] 算出来落在第 5 号桶,插入后你把它改成 [1, 2, 3],新的哈希结果可能应该去第 17 号桶,但对象还放在旧位置,查找就会混乱。所以 Python 干脆让 list、dict、set 这类可变对象不可哈希。
key 内容稳定 -> hash 稳定 -> dict 能定位
key 内容可变 -> hash 可能变化 -> dict 定位失效
六、可变性和性能也有关
不可变对象让很多行为更安全,比如字符串可以被共享、tuple 可以表达固定记录、可哈希对象可以放进 dict 和 set。但不可变也有代价:频繁“修改”会创建新对象。字符串拼接就是常见例子,小规模 s += part 没必要紧张,但大量循环拼接更推荐收集到列表后使用 "".join(parts)。
parts = []
for i in range(10000):
parts.append(str(i))
text = ",".join(parts)
可变对象则适合增量构建,比如列表逐步 append、字典逐步填充。设计 API 时要明确是“原地修改传入对象”,还是“返回一个新对象”,否则调用方很容易误判副作用边界。
七、常见误区与追问
- 误区:不可变对象就是变量不能重新赋值。 变量随时可以重新绑定,不可变说的是原对象内容不能原地改。
- 误区:tuple 绝对深度不可变。 tuple 的槽位不可变,但槽位指向的 list、dict 等对象仍可能原地变化。
- 误区:函数传不可变对象是值传递,传可变对象是引用传递。 Python 传参规则一致,差异来自对象是否能被原地修改。
- 追问:为什么 list 不能作为 dict key? list 可变且不可哈希,内容变化会破坏哈希表定位。
- 追问:字符串拼接为什么可能低效? 字符串不可变,大量循环拼接可能不断创建新字符串;批量拼接常用
join。 - 追问:可变对象一定不好吗? 不是;可变对象适合增量构建和维护状态,只是要清楚共享引用和副作用。
八、加强记忆
把可变性记成“对象能不能原地改”:list/dict/set 常见可变,适合增量更新;int/str/tuple 常见不可变,修改效果通常是创建新对象并重新绑定名字。回答时一定拆开变量、引用和对象三层,再连接到函数传参、默认参数、哈希 key、浅拷贝和字符串拼接这些高频场景。