
前言列表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.8lst [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.8print(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.8s {1, a, (2, 3)} # 合法# s {[1, 2]} # TypeError: unhashable type: listd {}d[(1, 2)] tuple key # 合法# d[[1, 2]] x # TypeError: unhashable type: listfs frozenset([1, 2]) # frozenset 不可变因此可哈希d[fs] frozenset keyprint(sorted(d, keystr)) # 能同时容纳元组键和 frozenset 键字典的值没有这个限制列表、字典都能当值。这条差异经常被混淆限制只针对键。四、取值方式下标 vs 键 vs 不能取列表按下标取字典按键取集合两者都不行。# 适用于 Python 3.8lst [10, 20, 30]print(lst[0]) # 10d {x: 10, y: 20}print(d[x]) # 10print(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.8big_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.8lst [1, 2]lst.append(3) # 列表有 append/extend/insert/remove/pop/sortst {1, 2}st.add(3) # 集合用 add / discard / remove / updatest.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) 1print(counts[a]) # 5字典做计数是最经典的用法键是元素值是出现次数。如果只想统计「出现过哪些」不需要 次数用集合就够了。八、核心区别总表维度列表 list集合 set字典 dict有序性有序位置固定无序顺序是细节3.7 起保证插入顺序可重复允许元素唯一键唯一值可重复下标访问支持不支持不支持按键访问切片支持不支持不支持元素/键可否哈希无要求必须可哈希键必须可哈希值无要求成员判断O(n)平均O(1)平均O(1)按下标/键取值O(1)不支持平均O(1)可变是是是可哈希自身否否否常用方法append、sort、popadd、discard、unionget、keys、update典型场景有序序列、排序去重、成员判断、交并差查表、计数、分组常见坑点坑点 1拿列表做高频成员判断。❌ 错误写法# 适用于 Python 3.8allowed [a, b, c, d]for x in [a, z]:if x in allowed: # 每次都线性扫描print(x)✅ 正确写法# 适用于 Python 3.8allowed {a, b, c, d} # 用集合成员判断平均是常数级for x in [a, z]:if x in allowed:print(x)坑点 2对集合按下标取值。❌ 错误写法# 适用于 Python 3.8st {10, 20}# print(st[0]) # TypeError: set object is not subscriptable✅ 正确写法# 适用于 Python 3.8st {10, 20}print(sorted(st)[0]) # 10需要位置就先转成有序序列坑点 3把{}当成空集合。❌ 错误写法# 适用于 Python 3.8s {}print(type(s)) # class dict这是空字典✅ 正确写法# 适用于 Python 3.8s 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.8d {(1, 2): x} # 元组做键s {frozenset({1, 2})} # frozenset 可哈希print(d, len(s)) # {(1, 2): x} 1坑点 5以为集合会保持插入顺序。❌ 错误写法# 适用于 Python 3.8seen {3, 1, 2}# 依赖 print(seen) 的顺序做逻辑判断✅ 正确写法# 适用于 Python 3.8seen {3, 1, 2}ordered sorted(seen) # 需要顺序就显式排序print(ordered) # [1, 2, 3]坑点 6遍历字典或集合时改大小。❌ 错误写法# 适用于 Python 3.8d {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.8d {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.8d {a: 1}# print(d[b]) # KeyError: b✅ 正确写法# 适用于 Python 3.8d {a: 1}print(d.get(b)) # Noneprint(d.get(b, 0)) # 0给个默认值if b in d:print(d[b])坑点 8往集合里塞「本该不同」的元素结果被静默合并。❌ 错误写法# 适用于 Python 3.8st {1, True, 1.0}print(len(st)) # 1因为 1 True 1.0哈希也相同✅ 正确写法# 适用于 Python 3.8st {1, True, 1.0}# 需要区分类型就别用集合或者先统一成唯一标识ids {num:1, bool:True, float:1.0}print(len(ids)) # 3集合靠「哈希相等 相等比较」判重数值相等的不同类型会被当成同一个。总结你的需求选谁要顺序、要下标、要保留重复列表要判断「在不在」、要去重、要交并差集合要按名字/编号查一个值、要计数分组字典既要顺序又要快速查找字典的键或列表 集合两手准备一句话概括三者的分工列表管顺序集合管唯一和存在性字典管映射。它们之间能互相 转换list()/set()/dict()但每次转换都要付出一次完整遍历的代价而且会 丢掉各自不擅长承载的信息——比如列表转集合就永远丢了顺序。选择时先问清楚这份数据 「顺序重要还是存在性重要」答案基本就出来了。