Python 的列表 list 和元组 tuple 有啥本质区别?啥时候用谁更合适?
Python 的列表 list 和元组 tuple 有啥本质区别?啥时候用谁更合适?
一句话先看结论
- 列表 list:能改、能增删,万能“购物车”,适合会变的序列数据。
- 元组 tuple:不能改,创建后就“锁死”,更省内存、更快读,适合固定不变的数据,比如常量配置、坐标、不可变键等。
为什么会这样?因为 tuple 不允许改动,解释器可做内存和访问优化;list 需要支持增删改,内部会额外预留空间并有更多开销。
语法与不可变性
- 列表:用方括号 [],可以随时改元素、append、pop。
- 元组:用圆括号 (),一旦创建不能改(改就报错 TypeError)。
示例:
# 列表:能改
fruits = ["apple", "banana"]
fruits.append("pear") # OK
fruits[^0] = "orange" # OK
print(fruits) # ['orange', 'banana', 'pear']# 元组:不能改
pos = (10, 20, 30)
# pos[^0] = 99 # TypeError: 'tuple' object does not support item assignment
print(pos) # (10, 20, 30)
这种“不可变”带来的副作用是:只要里面的元素也都不可变,元组就能当字典的键或放进 set,因为它可哈希;列表不行。
# 元组可做 dict 的键(元素都不可变时)
prices = {("apple", "small"): 3.5, ("apple", "large"): 5.0}
print(prices[("apple", "small")]) # 3.5# 列表做键会报错
# bad = {[1, 2]: "x"} # TypeError: unhashable type: 'list'
不可变还带来另一个好处:并发里更安全(没人能改它),也更可预测(不担心被无意修改)。
性能与内存:为什么 tuple 往往更省更快(读多改少场景)
- 内存:相同元素下,tuple 一般比 list 占用更少内存,因为它不需要为“未来的增长”预留空间,也没有可变带来的额外管理开销。
- 访问/迭代:tuple 往往稍快于 list,尤其“只读访问”场景,因为解释器可针对不可变对象做优化(比如更紧凑的布局、少一点边界检查等)。[2][3][1][5]
- 但注意:很多日常小规模场景下,这种差距并不大,写业务代码不必为每一次小访问纠结,先选语义更合适的数据结构才是正解。
简单对比(不同机器数值会不同,仅示意):
import sys, timedata_list = [i for i in range(100000)]
data_tuple = tuple(data_list)print("list bytes:", sys.getsizeof(data_list))
print("tuple bytes:", sys.getsizeof(data_tuple))
# 一般会看到 tuple 更小[^1][^7][^10]# 简单迭代耗时对比(多次取平均更客观)
t0 = time.perf_counter()
s = 0
for x in data_list:s += x
t1 = time.perf_counter()s = 0
for x in data_tuple:s += x
t2 = time.perf_counter()print("list iterate:", t1 - t0)
print("tuple iterate:", t2 - t1)
# 通常 tuple 稍快或相近,尤其纯读取场景[^1][^3][^6]
为什么会更省更快?大白话解释:
- list 是“可扩容的箱子”,需要留富余空位,随时支持 append/insert/pop,因此多点管理成本。
- tuple 是“密封好的盒子”,大小固定,解释器一次到位分配,更紧凑,拿数据更省事。
另外,tuple 在编译层面还有一些可优化场景,比如“常量折叠”:常量元组可在编译期直接预先构建,少走些指令。
什么时候用 list,什么时候用 tuple?
- 需要改动(增删改顺序)的序列:用 list,比如收集请求、动态队列、批量构建数据。
- 固定不变、只读的数据:用 tuple,比如常量配置、固定坐标、RGB 颜色、数据库字段定义等。
- 需要当 dict 的键或放入 set:用 tuple(且内部元素也要不可变)。
- 性能敏感、读多写少且数据大:倾向 tuple,省内存、迭代稍快。
- API 设计想表达“这是只读返回值”:返回 tuple 让调用者一看就知道不可改,更安全。
示例:配置项、坐标等固定数据用 tuple
# 常量配置
ALLOWED_ROLES = ("admin", "editor", "viewer") # 不希望被修改[^1][^6]# 坐标 / 颜色
origin = (0.0, 0.0)
white = (255, 255, 255)# 作为字典键
edge_weights = {("A", "B"): 1.2,("B", "C"): 0.8,
} # 键是不可变的元组,语义清晰[^6][^15]
示例:会变的数据用 list
# 任务队列(会变化)
tasks = []
tasks.append("crawl_page")
tasks.append("parse_html")
tasks.pop(0) # 处理一个
print(tasks)
一些容易踩坑的小点
- tuple 不可变,但“浅不可变”:如果元组里放的是可变对象,比如列表,那么列表的内容还是能改的;只是不能改“这个位置放的是谁”。
t = (1, [2, 3], 4)
t[^1].append(99) # 可以,因为改的是内部列表的内容
print(t) # (1, [2, 3, 99], 4)
# 但 t[^1] = [8, 9] 不行,会 TypeError
- 想“增加一个元素”的 tuple,本质是创建了一个新 tuple,并不会原地改旧的:
t = (1, 2, 3)
t2 = t + (4,) # 新建
print(t, "=>", t2) # (1, 2, 3) => (1, 2, 3, 4)
进阶细节:为什么 tuple 构建/读取可能更快?
- 不可变意味着解释器知道“大小和内容不变”,可一次性分配、更紧凑地存储,访问时开销更少;同时一些常量元组还能在编译时被折叠,减少运行期开销。
- list 为了支持动态增长,会“过度分配”预留空位,增删时需要移动或扩容,带来额外成本;但正因如此,它对频繁变动的场景更友好。
小结式指南
- 优先按语义选型:会变→list;不变→tuple。
- 想要更省内存、稍快迭代、可当 dict 键/放 set,且数据确实不需要改→tuple。
- 实际性能别猜,真有需求就用 time/perf_counter 和 sys.getsizeof 做小基准对比再决定。
参考要点基于公开资料的共识:tuple 不可变带来更好的内存紧凑与一定场景下的访问优势;list 提供灵活的增删改,是通用工作马。