# Python中的可变对象和不可变对象有什么区别?
面试官问:“Python中的可变对象和不可变对象有什么区别?”
很多录友会背出“int、str、tuple不可变,list、dict、set可变”,但被追问“x += 1到底改没改对象”“元组里放个列表还算不可变吗”,就答不上来了。这题的关键是分清两种操作:原地修改对象和让名字重新绑定到新对象。这一篇接着《Python变量保存的是什么?》往下讲。
# 简要回答
可变对象创建后,可以在保持身份不变的前提下原地修改它的值,常见的有list、dict、set和大多数自定义类的实例。不可变对象创建后值不能改变,常见的有int、float、bool、str、tuple、frozenset和bytes。
对不可变对象做“修改”,比如x = x + 1、text.upper(),得到的都是新对象,原对象不变;变量只是被重新绑定了。对可变对象调用append()、update()这类方法,改的是同一个对象,所有指向它的变量都会看到变化。
不可变还要注意边界:元组不可变,指的是它保存的引用不能换,如果元素本身是列表,列表内部仍然可以修改。这也是元组不一定可哈希的原因。
# 详细回答
# 判断标准:身份是否保持不变
判断一个操作改没改原对象,最直接的方法是看操作前后id()有没有变化:
count = 10
before = id(count)
count += 1
print(id(count) == before) # False,count绑定到了新对象11
scores = [90, 85]
before = id(scores)
scores.append(70)
print(id(scores) == before) # True,同一个列表被原地修改
2
3
4
5
6
7
8
9
int不可变,count += 1只能创建新对象11,再让count指向它。list可变,append()直接改动原列表,身份不变。

左边是不可变对象:名字换了指向,旧对象原封不动。右边是可变对象:对象本身被改了,所以所有绑定到它的名字都会看到新值。
# 共享可变对象时最容易踩坑
两个名字指向同一个可变对象时,从任意一个名字修改,另一个都会受影响:
config = {"debug": False}
backup = config
backup["debug"] = True
print(config) # {'debug': True}
2
3
4
backup = config没有复制字典,只是多了一个名字。很多人想“先备份再修改”,结果备份和原数据一起被改。真正要备份,需要创建新对象,比如config.copy();嵌套结构还要考虑浅拷贝和深拷贝的差异。
不可变对象就没有这个问题。字符串的所有“修改”方法都返回新字符串,原字符串不受影响,所以多个名字共享同一个字符串是安全的。字符串为什么设计成不可变,可以看《Python字符串为什么不可变?》。
# +=的行为取决于对象类型
+=经常被当作“原地加”,但它的行为取决于对象是否支持原地修改:
items = [1]
alias = items
items += [2]
print(alias) # [1, 2],列表被原地扩展
point = (1,)
alias = point
point += (2,)
print(alias) # (1,),元组创建了新对象
2
3
4
5
6
7
8
9
列表实现了原地加法,items += [2]相当于在原列表上扩展,alias也能看到。元组不支持原地修改,point += (2,)会创建新元组再重新绑定point,alias仍指向旧元组。
同样的+=,对可变对象是原地修改,对不可变对象是重新绑定。
# 元组的“不可变”有边界
元组不可变,是说它保存的引用不能增删、不能替换。它不保证元素指向的对象也不可变:
record = ("Python", [90, 85])
record[1].append(70)
print(record) # ('Python', [90, 85, 70])
record[1] = [] # TypeError: 'tuple' object does not support item assignment
2
3
4
5
record[1].append(70)没有修改元组,修改的是元组引用的那个列表。record[1] = []想替换元组里的引用,才会被拒绝。

图中可以看到,元组锁住的是槽位里的引用,绿色路径改的是被引用的列表,红色路径想换掉槽位里的引用,所以会抛出TypeError。
# 可变性和哈希的关系
字典的键和集合的元素要求可哈希:对象在生命周期内哈希值不能变化,并且能用==比较。可变的内置容器如list、dict、set不可哈希,因为内容一变,哈希值就会失效,字典就找不到原来的键。
元组是否可哈希取决于元素:(1, 2)可以作为字典键,([1, 2], 3)调用hash()会抛出TypeError: unhashable type: 'list'。所以“不可变对象一定可哈希”这个说法也不准确。
# 知识拓展
面试官可能追问:
Q1:既然字符串不可变,为什么s += "a"能执行成功?
因为这行代码没有修改原字符串,而是创建了一个新字符串,再让s重新绑定过去。操作前后看id(s),通常会发现身份变了。循环里反复拼接字符串会不断产生新对象,大量拼接更推荐收集到列表后用"".join()。
Q2:函数内修改传入的列表,为什么外面也变了?
调用函数时,形参和实参绑定到同一个对象。函数内append()是原地修改,外面看到的是同一个列表;函数内写items = []则是重新绑定形参,不会影响外面。这正是可变性和名字绑定在函数调用中的体现。
Q3:不可变对象有什么好处?
不可变对象可以被多个名字安全共享,不用担心被意外修改;值稳定也让它适合作为字典键和集合元素。在CPython中,解释器还会对部分不可变对象做复用优化,但这只是实现细节,判断相等时仍应使用==,原因见《is和==有什么区别?》。
Q4:列表和元组除了可变性,还有哪些区别?
还涉及哈希、内存占用和使用语义:元组常用来表示结构固定的一组数据,列表表示同类元素的可变集合。完整对比见《Python列表和元组有什么区别?》。
评论
验证登录状态...