前言
列表(list)、集合(set)、字典(dict)是 Python 用得最多的三个内置容器,但很多人在 选的时候是凭手感:反正都能装东西,随便挑一个能跑就行。结果常见的是拿列表做成员判断, 数据一多就慢得离谱;或者拿集合去按顺序取值,一取就报not subscriptable。这三个容器 的差别不是风格问题,而是数据结构层面的取舍,选错了会直接体现在正确性和效率上。
本文按七个维度横向对比:有没有顺序、能不能重复、元素能不能被哈希、能不能按下标取值、 查找要多少代价、能不能改、典型用途是什么。每个维度都配可运行的例子,最后一节给一张总 表。
先确定版本基准:Python 2.7 已于 2020 年 1 月 1 日停止维护,本文使用 Python 3 的写法, 示例适用于 Python 3.8 及以上。文中的复杂度指CPython 实现下的代价,其他实现可能 不同;涉及「平均情况」的地方会注明。字典「有序」指的是 Python 3.7 起保证按插入顺序 迭代(3.6 只是 CPython 的实现细节,不要依赖)。
一、有序性:三者完全不同
- 列表有序:元素按插入顺序排列,位置固定,可以按下标取值。
- 集合无序:集合不记录插入顺序,迭代出来的顺序是实现细节,不要依赖。
- 字典有序:3.7 起保证按插入顺序迭代;更新已存在的键不会改变它的位置。
# 适用于 Python 3.8+
lst = [3, 1, 3]
st = {3, 1, 3}
d = {"a": 1, "b": 2}
print(lst) # [3, 1, 3] 顺序就是插入顺序
print(sorted(st)) # [1, 3] 要确定顺序得自己排序
print(list(d)) # ['a', 'b'] 键按插入顺序注意sorted(st)只是把集合转成排好序的列表,并没有让集合本身变得有序。集合永远 没有「第几个元素」这个概念。
二、可重复性:列表允许,集合和字典的键不允许
列表对重复元素毫无意见,长度是多少就是多少。集合的每个元素只能出现一次,字典的每个键 也只能出现一次(值可以重复,也可以不可哈希)。
# 适用于 Python 3.8+
print(len([1, 1, 2, 2, 2])) # 5,全留着
print(len({1, 1, 2, 2, 2})) # 2,重复的自动合并
d = {"a": 1, "b": 1}
print(len(d)) # 2,值相同没关系,键不同即可「元素唯一」是集合最核心的性质,去重、求交并差都靠它。但也正因为唯一,往集合里放重复 值不会有任何提示,是静默合并——如果不小心的元素本来不该相等,问题会藏得很深。
三、可哈希性:集合元素和字典键的门槛
要放进集合、或者当字典的键,对象必须可哈希(hashable)。可哈希的对象在它的生命周期 内哈希值不变,并且能和其他对象比较相等。基本类型(整数、字符串、元组、frozenset) 都可哈希;列表、字典、集合这类可变对象都不可哈希。
# 适用于 Python 3.8+
s = {1, "a", (2, 3)} # 合法
# s = {[1, 2]} # TypeError: unhashable type: 'list'
d = {}
d[(1, 2)] = "tuple key" # 合法
# d[[1, 2]] = "x" # TypeError: unhashable type: 'list'
fs = frozenset([1, 2]) # frozenset 不可变,因此可哈希
d[fs] = "frozenset key"
print(sorted(d, key=str)) # 能同时容纳元组键和 frozenset 键字典的值没有这个限制,列表、字典都能当值。这条差异经常被混淆:限制只针对键。
四、取值方式:下标 vs 键 vs 不能取
列表按下标取,字典按键取,集合两者都不行。
# 适用于 Python 3.8+
lst = [10, 20, 30]
print(lst[0]) # 10
d = {"x": 10, "y": 20}
print(d["x"]) # 10
print(d.get("z")) # None,键不存在时 get 返回 None,不抛异常
st = {10, 20, 30}
# print(st[0]) # TypeError: 'set' object is not subscriptable集合要用「在不在」的方式访问:x in st。它不提供「第几个」这种位置概念,也没有__getitem__。
五、查找代价:这是最该记住的一条
成员判断的代价差别极大,官方文档给出的复杂度是:
| 操作 | 复杂度 |
|---|
x in lst(列表成员判断) | O(n) |
x in st(集合成员判断) | 平均O(1),最坏O(n) |
key in d(字典键判断) | 平均O(1),最坏O(n) |
lst[i](列表按下标取) | O(1) |
d[key](字典按键取) | 平均O(1),最坏O(n) |
# 适用于 Python 3.8+
big_list = list(range(100000))
big_set = set(range(100000))
print(99999 in big_list) # True,但要把列表扫一遍
print(99999 in big_set) # True,平均只需一次定位上面两个表达式结果相同,代价却差着量级。判断「在不在」时优先用集合或字典的键。「最坏O(n)」指的是所有键都哈希到同一个值这种极端情况,正常散列下不会发生。
六、可变性:三者都能改,但方式不同
三个容器都是可变的,但修改的入口不一样。
# 适用于 Python 3.8+
lst = [1, 2]
lst.append(3) # 列表有 append/extend/insert/remove/pop/sort
st = {1, 2}
st.add(3) # 集合用 add / discard / remove / update
st.discard(1)
d = {"a": 1}
d["b"] = 2 # 字典按键赋值
d.update({"c": 3})
print(lst, st, d) # [1, 2, 3] {2, 3} {'a': 1, 'b': 2, 'c': 3}列表有位置概念,所以有insert、pop(i)这类按下标的方法;集合没有位置,只有 「加一个 / 去掉一个 / 批量化」;字典则是「按键」存取。
七、典型用途
| 容器 | 典型用途 |
|---|
| 列表 | 有序记录、需要排序、需要按下标或切片访问、需要保留重复项 |
| 集合 | 去重、成员判断、交并差运算、给「已见过」的东西做索引 |
| 字典 | 按键查表、计数、分组、映射关系、需要给每个键挂一个值 |
# 适用于 Python 3.8+
# 列表:保序、允许重复
scores = [88, 92, 88]
# 集合:去重 + 快速判断
unique_ids = {101, 102, 103}
print(102 in unique_ids) # True
# 字典:计数
counts = {}
for c in "abracadabra":
counts[c] = counts.get(c, 0) + 1
print(counts["a"]) # 5字典做计数是最经典的用法:键是元素,值是出现次数。如果只想统计「出现过哪些」,不需要 次数,用集合就够了。
八、核心区别总表
| 维度 | 列表 list | 集合 set | 字典 dict |
|---|
| 有序性 | 有序,位置固定 | 无序,顺序是细节 | 3.7 起保证插入顺序 |
| 可重复 | 允许 | 元素唯一 | 键唯一,值可重复 |
| 下标访问 | 支持 | 不支持 | 不支持(按键访问) |
| 切片 | 支持 | 不支持 | 不支持 |
| 元素/键可否哈希 | 无要求 | 必须可哈希 | 键必须可哈希,值无要求 |
| 成员判断 | O(n) | 平均O(1) | 平均O(1) |
| 按下标/键取值 | O(1) | 不支持 | 平均O(1) |
| 可变 | 是 | 是 | 是 |
| 可哈希(自身) | 否 | 否 | 否 |
| 常用方法 | append、sort、pop | add、discard、union | get、keys、update |
| 典型场景 | 有序序列、排序 | 去重、成员判断、交并差 | 查表、计数、分组 |
常见坑点
坑点 1:拿列表做高频成员判断。
❌ 错误写法:
# 适用于 Python 3.8+
allowed = ["a", "b", "c", "d"]
for x in ["a", "z"]:
if x in allowed: # 每次都线性扫描
print(x)✅ 正确写法:
# 适用于 Python 3.8+
allowed = {"a", "b", "c", "d"} # 用集合,成员判断平均是常数级
for x in ["a", "z"]:
if x in allowed:
print(x)坑点 2:对集合按下标取值。
❌ 错误写法:
# 适用于 Python 3.8+
st = {10, 20}
# print(st[0]) # TypeError: 'set' object is not subscriptable✅ 正确写法:
# 适用于 Python 3.8+
st = {10, 20}
print(sorted(st)[0]) # 10,需要位置就先转成有序序列坑点 3:把{}当成空集合。
❌ 错误写法:
# 适用于 Python 3.8+
s = {}
print(type(s)) # <class 'dict'>,这是空字典✅ 正确写法:
# 适用于 Python 3.8+
s = set()
print(type(s), len(s)) # <class 'set'> 0空集合只能写set(),{}是空字典。
坑点 4:把不可哈希的对象当字典键或集合元素。
❌ 错误写法:
# 适用于 Python 3.8+
# d = {[1, 2]: "x"} # TypeError: unhashable type: 'list'
# s = {{1, 2}} # TypeError: unhashable type: 'set'✅ 正确写法:
# 适用于 Python 3.8+
d = {(1, 2): "x"} # 元组做键
s = {frozenset({1, 2})} # frozenset 可哈希
print(d, len(s)) # {(1, 2): 'x'} 1坑点 5:以为集合会保持插入顺序。
❌ 错误写法:
# 适用于 Python 3.8+
seen = {3, 1, 2}
# 依赖 print(seen) 的顺序做逻辑判断✅ 正确写法:
# 适用于 Python 3.8+
seen = {3, 1, 2}
ordered = sorted(seen) # 需要顺序就显式排序
print(ordered) # [1, 2, 3]坑点 6:遍历字典或集合时改大小。
❌ 错误写法:
# 适用于 Python 3.8+
d = {"a": 1, "b": 2, "c": 3}
# for k in d:
# if d[k] < 2:
# del d[k] # RuntimeError: dictionary changed size during iteration✅ 正确写法:
# 适用于 Python 3.8+
d = {"a": 1, "b": 2, "c": 3}
for k in list(d): # 先固化一份键的快照再遍历
if d[k] < 2:
del d[k]
print(d) # {'b': 2, 'c': 3}坑点 7:字典取值误用[]处理可能不存在的键。
❌ 错误写法:
# 适用于 Python 3.8+
d = {"a": 1}
# print(d["b"]) # KeyError: 'b'✅ 正确写法:
# 适用于 Python 3.8+
d = {"a": 1}
print(d.get("b")) # None
print(d.get("b", 0)) # 0,给个默认值
if "b" in d:
print(d["b"])坑点 8:往集合里塞「本该不同」的元素,结果被静默合并。
❌ 错误写法:
# 适用于 Python 3.8+
st = {1, True, 1.0}
print(len(st)) # 1,因为 1 == True == 1.0,哈希也相同✅ 正确写法:
# 适用于 Python 3.8+
st = {1, True, 1.0}
# 需要区分类型就别用集合,或者先统一成唯一标识
ids = {"num:1", "bool:True", "float:1.0"}
print(len(ids)) # 3集合靠「哈希相等 + 相等比较」判重,数值相等的不同类型会被当成同一个。
总结
| 你的需求 | 选谁 |
|---|
| 要顺序、要下标、要保留重复 | 列表 |
| 要判断「在不在」、要去重、要交并差 | 集合 |
| 要按名字/编号查一个值、要计数分组 | 字典 |
| 既要顺序又要快速查找 | 字典的键,或列表 + 集合两手准备 |
一句话概括三者的分工:列表管顺序,集合管唯一和存在性,字典管映射。它们之间能互相 转换(list()/set()/dict()),但每次转换都要付出一次完整遍历的代价,而且会 丢掉各自不擅长承载的信息——比如列表转集合就永远丢了顺序。选择时先问清楚:这份数据 「顺序重要还是存在性重要」,答案基本就出来了。