← 返回题目列表

Python 中可变对象和不可变对象有什么区别?

高频 简单 第 6 / 22 题 更新于 2026/07/25
Python可变对象不可变对象对象模型

简化版

可变对象创建后内容还能原地修改,比如 listdictset;不可变对象创建后值不能原地改变,比如 intfloatstrtuple。区别的关键不是变量能不能重新赋值,而是对象本身能不能被原地修改。

详细版

常见分类:

类型是否可变示例
intfloatbool不可变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 = 新对象 永远只是重新绑定名字。

传入对象函数内操作外部是否可见
listappend 原地修改可见
listnums = [] 重新绑定不可见
intn += 1 新建并绑定不可见
dictd["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、浅拷贝和字符串拼接这些高频场景。