Python 中 list、tuple、dict、set 有什么区别?
简化版
list 是有序可变序列,适合按位置存放一组元素;tuple 是不可变序列,适合表达固定结构;dict 是键值映射,适合按 key 快速查值;set 是无重复集合,适合去重和集合运算。
详细版
四个基础容器对比:
| 类型 | 是否有序 | 是否可变 | 是否允许重复 | 典型用途 |
|---|---|---|---|---|
list | 有序 | 可变 | 允许 | 动态列表、队列雏形、批量数据 |
tuple | 有序 | 不可变 | 允许 | 固定记录、函数多返回值 |
dict | 保持插入顺序 | 可变 | key 不重复 | 配置、对象属性、映射关系 |
set | 不保证按业务顺序使用 | 可变 | 不允许 | 去重、交并差运算、成员判断 |
例子:
names = ["Tom", "Jerry"]
point = (10, 20)
user = {"id": 1, "name": "Tom"}
tags = {"python", "backend", "interview"}
常见选择:
- 要按下标访问、元素会变:选
list。 - 表达固定结构、不希望被改:选
tuple。 - 要按 key 查值:选
dict。 - 要去重、判断是否存在、做交集并集:选
set。
完整版教学
一、先用四个问题选容器
Python 这四个容器不是互相替代的关系,而是面向不同访问方式。选型时先问四个问题:是否需要顺序,是否需要修改,是否需要 key-value 映射,是否需要去重或集合运算。把问题问对,比背“list 有序、dict 映射”更接近真实开发。
| 类型 | 核心语义 | 是否可变 | 重复元素 | 常见复杂度直觉 |
|---|---|---|---|---|
list | 有序序列 | 是 | 允许 | 下标访问 O(1),中间插删 O(n) |
tuple | 固定序列/记录 | 否 | 允许 | 下标访问 O(1),不能原地改 |
dict | key -> value 映射 | 是 | key 不重复 | 按 key 查找平均 O(1) |
set | 无重复集合 | 是 | 不允许 | 成员判断平均 O(1) |
这些复杂度是常见哈希表和动态数组实现下的平均认知,不代表每个操作都没有代价。比如 list.insert(0, x) 要移动后面的元素,10 万个元素头部插入一次就可能移动接近 10 万个引用;而 x in big_set 通常不需要线性扫描。
二、list:最常用的可变序列
list 适合保存一组有顺序的数据:
nums = [1, 2, 3]
nums.append(4)
nums[0] = 100
它的特点:
- 支持下标访问;
- 支持切片;
- 支持增删改;
- 保留元素顺序;
- 可以存放不同类型对象。
常见操作:
nums.append(5)
nums.extend([6, 7])
nums.pop()
nums.sort()
面试补充:list 底层可以理解为动态数组思想,保存的是一段连续的对象引用。按下标访问很快,因为能直接计算位置;尾部 append 通常很快,因为列表会预留容量;但在中间插入或删除通常需要移动元素。比如长度为 10000 的列表,在下标 0 插入一个元素,后面约 10000 个引用都要后移。
插入前: [A, B, C, D]
insert(0, X)
插入后: [X, A, B, C, D]
后面元素整体右移
所以 list 适合“有顺序、会变化、需要按位置访问”的数据;如果频繁在两端操作队列,应考虑 collections.deque,不要把 list 当万能队列。
记忆钩子:list 的强项是“按位置组织一串会变化的数据”,不是所有顺序数据都必须用 list,固定记录和双端队列都有更合适的工具。
三、tuple:固定结构更合适
tuple 是不可变序列:
point = (3, 4)
x, y = point
它常用于:
- 函数返回多个值;
- 表示固定结构,例如坐标、日期片段;
- 作为 dict key 的一部分,前提是内部元素也可哈希;
- 避免调用方误修改。
注意:
t = ([1, 2], 3)
t[0].append(4)
print(t) # ([1, 2, 4], 3)
tuple 自身不可变,但它引用的可变对象内容仍可能改变。准确说,tuple 的“槽位绑定”不能变:第 0 位仍然指向原来的列表对象;只是那个列表对象内部发生了变化。这个细节在“tuple 能否作为 dict key”时尤其重要:(1, 2) 可哈希,([1], 2) 不可哈希,因为里面包含不可哈希的 list。
四、dict:按 key 查 value
dict 是 Python 里非常核心的数据结构:
user = {
"id": 1,
"name": "Tom",
}
print(user["name"])
它适合表达映射关系:
用户 id -> 用户对象
配置名 -> 配置值
单词 -> 出现次数
从 Python 3.7 开始,字典的插入顺序成为语言保证。因此遍历 dict 时会按插入顺序返回 key,但它的核心语义仍然是 key-value 映射,不要把它当成专门的排序结构。dict 的 key 必须可哈希且相等语义稳定,否则哈希表无法可靠定位桶位。
scores = {}
scores["Tom"] = 95
scores["Tom"] = 98
print(scores) # {'Tom': 98}
这里不是保存了两个 Tom,而是同一个 key 的 value 被更新。用 dict 统计词频时,10 万个单词逐个扫描,平均每次更新是 O(1) 级别,比每次去列表里找某个单词的位置更合适。
五、set:去重和集合运算
set 的核心特点是元素不重复:
nums = [1, 2, 2, 3]
unique_nums = set(nums)
print(unique_nums) # {1, 2, 3}
集合运算很适合面试题:
a = {"java", "python", "go"}
b = {"python", "rust"}
print(a & b) # 交集 {'python'}
print(a | b) # 并集
print(a - b) # 差集
成员判断也很常用:
if user_id in allowed_ids:
...
当 allowed_ids 很大时,set 的成员判断通常比 list 线性扫描更合适。比如 100000 个 id 中判断某个 id 是否存在,list 最坏要比较 100000 次;set 通过哈希定位,平均不需要这么扫。set 不适合保存业务顺序,虽然迭代时会有某种顺序,但不应该把它当成稳定排序结果。
六、选型时看访问方式
可以按问题选容器:
- 我需要按位置访问吗?
list或tuple。 - 数据会不会变?会变用
list,固定结构用tuple。 - 我需要 key-value 吗?用
dict。 - 我只关心是否存在、是否重复吗?用
set。 - 我需要集合交并差吗?用
set。 - 我需要保留插入顺序并按 key 查吗?用
dict。
还要注意“可变性”和“共享引用”会影响容器行为。list.copy()、切片、dict.copy() 都是浅拷贝,外层容器换了,里面的可变对象可能仍共享。容器里放的不是“值的深度副本”,而是对象引用,这能解释很多看起来诡异的联动修改。
users = [{"name": "Tom"}]
copied = users.copy()
copied[0]["age"] = 18
print(users) # [{'name': 'Tom', 'age': 18}]
七、常见误区与追问
- 误区:tuple 里面任何东西都不能变。 tuple 的槽位不可变,但槽位指向的可变对象内容仍可能被修改。
- 误区:dict 有顺序后就可以替代 list。 dict 保持插入顺序,但核心是映射;按位置访问、切片、排序语义仍然不是它的主场。
- 误区:set 是“自动排序去重”。 set 负责去重和集合运算,不保证按业务需要排序;需要排序要显式
sorted()。 - 追问:为什么 list 查找成员通常比 set 慢? list 要线性比较元素,set 通过哈希定位,平均查找更快,但要求元素可哈希。
- 追问:dict 的 key 为什么不能用 list? list 可变且不可哈希,内容一变哈希位置就不稳定,dict 无法可靠查找。
- 追问:函数多返回值为什么常说是 tuple?
return a, b实际返回一个 tuple,调用方可以用解包语法接收。
八、加强记忆
把四个容器记成“四种工具”:list 管有序且会变化的一串对象,tuple 管固定结构和不可变槽位,dict 管从 key 到 value 的快速映射,set 管去重、成员判断和交并差。面试回答时围绕访问方式、可变性、是否去重、是否需要 key 查找四条线展开,比只背定义更稳。