列表和元组,到底用哪一个?
如果你写过几天 Python,一定被这个问题绊过:明明都能用 [] 和 () 装一堆东西,为什么有时候用 list,有时候又用 tuple?更气人的是,面试还爱问。
其实答案没那么玄学。一句话先给出来:list 是”可变的数据集合”,tuple 是”不可变的数据结构”。下面把这句话拆开讲透。
一、最本质的区别:能不能改
list 创建之后可以随意增删改:
nums = [1, 2, 3]nums.append(4) # 加nums[0] = 99 # 改nums.remove(2) # 删print(nums) # [99, 3, 4]tuple 一旦创建就定型了,任何试图修改元素的操作都会报错:
point = (1, 2)point[0] = 9 # TypeError: 'tuple' object does not support item assignment注意,是”元素不能换”,不是”里面一切都不能动”。如果 tuple 里套了一个 list,那个 list 本身还是能改的——但这通常是设计上的坏味道:
mixed = (1, [2, 3])mixed[1].append(4) # 能跑,但语义上很别扭print(mixed) # (1, [2, 3, 4])⚠️ 经验之谈:如果为了让 tuple “看起来可变”而往里塞 list/dict,多半说明你该用 list,甚至该用 dataclass。
二、不可变到底带来了什么好处?
1. 能当字典的键 / 集合的元素
这是最实在的区别。字典的键必须是**可哈希(hashable)**的,而可哈希的前提是不可变。
# 用 tuple 做坐标键,没问题visited = {}visited[(3, 4)] = Trueprint((3, 4) in visited) # True
# 用 list 做键,直接报错bad = {}bad[[3, 4]] = True # TypeError: unhashable type: 'list'所以当你需要把”一组值”当成一个整体去查重、当索引时(比如坐标、RGB 颜色、缓存 key),tuple 是唯一选择。
2. 更省内存、创建更快
tuple 结构比 list 简单,CPython 里分配和访问都更轻。数据量大或者频繁创建时,差异能感知到:
import sys, timeit
print(sys.getsizeof((1, 2, 3))) # 比同长度 list 小print(sys.getsizeof([1, 2, 3]))
# 创建 1000 万次的耗时对比t = timeit.timeit("(1,2,3,4,5)", number=10_000_000)l = timeit.timeit("[1,2,3,4,5]", number=10_000_000)print(f"tuple: {t:.3f}s list: {l:.3f}s")日常代码里这点差距基本可忽略,但在热点循环里,tuple 是更稳的选择。
3. 默认更安全
因为不能改,tuple 在函数间传来传去时,你不用担心它被谁偷偷改掉——这对并发、缓存、配置项都很有意义。
三、真正决定选哪个的:语义
性能和哈希都是”结果”,最该想清楚的是语义。
- tuple = 异构的、固定结构的数据记录。它装的是”不同的东西”,而且数量固定。像数据库的一行、一个点的坐标、一条 RGB 颜色。
- list = 同构的、数量可变的数据集合。它装的是”一堆相同的东西”,长度会变化。像购物车、待办事项、一组用户名。
看几个例子就懂了:
# tuple:异构、固定 → 这是"一个点"point = (10, 20)# tuple:异构、固定 → 这是"一条记录"record = ("张三", 28, "北京")
# list:同构、会变 → 这是"一群用户"users = ["张三", "李四", "王五"]# list:同构、会变 → 这是"购物车"cart = ["键盘", "鼠标", "显示器"]判断口诀很简单:问自己”这个序列的长度会变吗?里面的元素是一类东西吗?”
- 会变 / 是一类东西 → list
- 不变 / 是不同字段凑一起 → tuple
四、一张决策表
| 场景 | 选哪个 | 原因 |
|---|---|---|
| 待办清单、购物车 | list | 数量动态变化 |
| 函数返回多个值 | tuple | 本质就是返回了个 tuple |
| 字典的键 / 集合元素 | tuple | 需要可哈希 |
| 坐标、RGB、数据库行 | tuple | 异构固定结构 |
| 循环里频繁创建的常量序列 | tuple | 更省、更快 |
需要 .append / .sort | list | 需要可变操作 |
小知识:Python 函数
return a, b返回的其实就是一个 tuple,只是语法糖省略了括号。
五、现代写法:别再裸写 tuple 了
如果 tuple 用来承载”有名字的字段”(比如一条记录),裸 tuple 可读性很差——record[0] 谁知道是名字还是年龄?
优先用更语义化的写法:
from collections import namedtuplefrom typing import NamedTuplefrom dataclasses import dataclass
# 方式一:namedtuple(轻量、仍是 tuple)User = namedtuple("User", ["name", "age", "city"])u = User("张三", 28, "北京")print(u.name) # 比 u[0] 清楚多了
# 方式二:typing.NamedTuple(带类型注解,推荐)class User(NamedTuple): name: str age: int city: str
# 方式三:dataclass(要可变、要方法时用)@dataclassclass User: name: str age: int city: str规则:不可变 + 简单字段 → NamedTuple;需要修改或带行为 → dataclass。
六、两个常见误区
误区 1:“tuple 比 list 快,所以一律用 tuple。” 错。如果数据要频繁增删改,tuple 反而会逼你不停创建新对象,比 list 还慢。性能优势只在”创建后不再改”时才成立。
误区 2:“tuple 里不能放 list。” 能放,而且解释器不拦你。但一旦这么干,tuple 的”不可变保证”就破了,等于自己给自己挖坑。真要嵌套可变结构,老老实实上 dataclass 或 pydantic。
七、一句话总结
list 装”一堆会变的同类”,tuple 装”一组固定的异类”。拿不准时,先想语义,再想性能。
如果你愿意,下一篇可以聊聊 dict 和那些容易被忽略的小众容器(defaultdict、Counter、deque)——它们才是让代码变优雅的真正利器。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!