# Python列表和元组有什么区别?
面试官问:“Python中的列表和元组有什么区别?”
很多录友答完“列表可变、元组不可变”就停了。面试官接着问一句“那元组一定能当字典键吗”“元组里放列表,列表能改吗”,答案就开始含糊。这道题的关键是讲清不可变到底约束了什么,以及它怎样影响哈希、内存和使用场景。可变与不可变的通用规则见《Python中的可变对象和不可变对象有什么区别?》。
# 简要回答
列表list和元组tuple都是有序序列,都支持索引、切片、遍历和in判断。核心区别是:列表可变,可以原地增删改元素;元组不可变,创建后不能增删元素,也不能把某个位置换成别的对象。
这个区别带来几个直接结果。元组的元素都可哈希时,元组本身才可哈希,可以作为字典键或集合元素;列表永远不可哈希。在CPython中,元组没有为扩容预留空间,同样元素数量下通常比列表占用更少内存。
语义上,列表适合放数量会变化的同类数据,例如用户列表;元组适合放结构固定的一组值,例如坐标、数据库的一行记录、函数返回的多个值。
# 详细回答
# 原地修改与创建新对象
列表的append、索引赋值、sort都会原地修改同一个对象;元组没有这些修改方法,想“改”只能创建新元组:
courses = ["Python", "Java"]
alias = courses
courses.append("Go")
print(alias) # ['Python', 'Java', 'Go'],alias看到同一个列表的变化
point = (3, 5)
# point[0] = 10 # TypeError: 'tuple' object does not support item assignment
moved = (10,) + point[1:]
print(point, moved) # (3, 5) (10, 5)
2
3
4
5
6
7
8
9
alias和courses指向同一个列表,append修改的是这个列表本身,所以两个名字都能看到"Go"。元组point没有被修改,moved是拼接出来的新元组。
# 元组不可变,不等于里面的对象不可变
元组只保证每个位置保存的引用不变,不管被引用的对象自己能不能变:
record = ("Alice", [90, 85])
record[1].append(100)
print(record) # ('Alice', [90, 85, 100])
# record[1] = [60] # TypeError,不能把这个位置换成别的对象
print(hash(("Alice", 90))) # 正常输出一个整数
# hash(record) # TypeError: unhashable type: 'list'
2
3
4
5
6
7
record[1]仍然是原来那个列表,只是列表内容变了,元组的约束没有被打破。也正因为里面有可变的列表,record的内容可能随时变化,所以它不能计算哈希,不能当字典键。

图里要区分两层:元组这一层的“槽位引用”被锁死,替换会报错;槽位指向的列表那一层仍然可变,append可以成功。哈希要求整个值都稳定,只要有一层能变,元组就不能哈希。
# 内存占用与性能
在CPython中,列表为了让append的均摊时间复杂度保持O(1),扩容时会多申请一些空间;元组长度固定,不需要预留。以64位的CPython 3.13为例:
import sys
print(sys.getsizeof([1, 2, 3, 4, 5])) # 104
print(sys.getsizeof((1, 2, 3, 4, 5))) # 80
2
3
4
sys.getsizeof只统计容器本身,不包含元素对象,具体数值随版本和平台变化。可以确认的趋势是:同样元素数量下,元组通常更省内存;CPython还会把全部由常量组成的元组字面量直接放进代码对象,创建时开销更小。
但不要把“元组更快”当成换掉列表的理由。两者按下标访问都是O(1),真实程序的耗时通常不在这里。先按语义选类型,再考虑这点性能差异。
# 怎么选
| 对比项 | 列表 list | 元组 tuple |
|---|---|---|
| 可变性 | 可变,支持增删改 | 不可变,不能替换元素 |
| 可哈希 | 不可哈希 | 元素都可哈希时可哈希 |
| 内存(CPython) | 有扩容预留 | 长度固定,通常更省 |
| 典型语义 | 数量会变的同类数据 | 结构固定的一组值 |
| 常见场景 | 待办任务、查询结果集合 | 坐标、字典的复合键、多返回值 |
一个实用的判断方法:如果每个位置都有固定含义(第0位是姓名,第1位是年龄),用元组;如果每个元素地位相同、数量会变,用列表。字典键要用复合值时,例如(城市, 日期),只能用元组,字典对键的要求见《Python字典和集合有什么区别?》。
# 知识拓展
面试官可能追问:
Q1:函数return a, b返回的是什么?
返回的是一个元组,括号可以省略,真正构成元组的是逗号。调用方写name, age = get_user()时,是把元组解包给两个名字。解包和星号收集的更多写法见《Python切片和序列解包怎么用?》。
Q2:只有一个元素的元组怎么写?
要写成(42,)。(42)里的括号只是分组,结果还是整数42。空元组写作()或tuple()。
Q3:tuple(some_list)会把里面的列表也变成不可变吗?
不会。转换只创建一个新的元组容器,元素引用和原列表里的完全相同。原列表中如果有嵌套列表,转换后它依旧是同一个可变列表,修改会同时体现在原列表和新元组里。
Q4:元组+=为什么不报错?
t += (3,)对元组来说是创建一个新元组,再让t重新绑定过去,原元组没变。如果另一个名字也指向原元组,它看到的还是旧内容。列表+=则是原地扩展,和extend效果一样,所有指向该列表的名字都会看到变化。
评论
验证登录状态...