
前言字典dict和集合set是 Python 里两个「长得像、底子也一样」的内置类型。它们都用花括号写都基于哈希表hash table实现查找、插入的平均复杂度都是 O(1)。也正因为如此很多人会问既然底子一样为什么还要分两个关键差别只有一条字典存的是「键值对」集合只存「键」。换句话说集合就是「只有键、没有值」的字典——它只关心「某个东西在不在里面」不关心它对应什么。本文把这两者摆在一起对照着讲它们共同的哈希表基础、各自能做什么、成员判断、去重、以及只有集合才有的集合运算。一个容易搞错的事实要先说清字典从 Python 3.7 起保持插入顺序是有序的而集合不保证顺序不要指望set的遍历顺序稳定。一、共同的底子哈希表哈希表通过hash()把元素映射到一个位置。要当字典的键或集合的元素对象必须可哈希hashable即它有稳定的哈希值且相等就哈希相等。数字、字符串、元组元素也都可哈希时都行列表、字典、集合本身不行。# 适用于 Python 3.8print(hash(hello) hash(hello)) # True同样的内容哈希相同print(hash(1) hash(1.0) hash(True)) # True相等即同哈希d {[1, 2]: x} # ❌ TypeError: unhashable type: lists {[1, 2]} # ❌ 同理集合元素也不能用列表正因为都靠哈希定位二者的成员判断都极快# 适用于 Python 3.8d {apple: 1, banana: 2}s {apple, banana}print(apple in d) # True判断键print(apple in s) # True判断元素二、一个存键值对一个只存键字典把「键」映射到「值」值可以是任意对象且可以重复# 适用于 Python 3.8scores {数学: 95, 语文: 95, 英语: 79}print(scores[数学]) # 95print(scores[语文]) # 95值可以重复集合只保留「东西本身」自动去重# 适用于 Python 3.8tags {python, python, dict}print(tags) # {python, dict}重复的被去掉print(len(tags)) # 2集合不能按下标取值因为它没有「位置」的概念# 适用于 Python 3.8s {a, b}print(s[0]) # ❌ TypeError: set object is not subscriptable特性字典 dict集合 set存什么键 → 值只有元素花括号写法{a: 1}{a}空对象写法{}set()是否有序3.7 起保持插入顺序不保证顺序可否下标取值键可以d[k]不能自动去重键去重值不去重元素去重能否做集合运算键视图可以原生支持三、去重与成员判断去重是集合最出名的用途。给一个含重复元素的列表转成集合再转回来即可# 适用于 Python 3.8nums [1, 2, 2, 3, 3, 3]unique list(set(nums))print(unique) # 顺序不保证内容为 1、2、3# 想保留原顺序用 dict3.7 起保持插入顺序unique_ordered list(dict.fromkeys(nums))print(unique_ordered) # [1, 2, 3]最后一行是个很实用的技巧dict.fromkeys(nums)用列表元素当键建字典键自动去重而字典从 3.7 起保持插入顺序所以能保留原顺序——这是set做不到的。成员判断方面二者都很快但语义不同字典判断的是键集合判断的是元素本身。# 适用于 Python 3.8d {a: 1, b: 2}print(1 in d) # False1 是值不是键print(1 in d.values()) # True判断值要显式取 values()四、集合运算集合有四个运算运算符和对应方法都可以用并集|、交集、差集-、对称差^。# 适用于 Python 3.8a {python, java, go}b {java, go, rust}print(a | b) # 并集{python, java, go, rust}print(a b) # 交集{java, go}print(a - b) # 差集a 有而 b 没有{python}print(a ^ b) # 对称差只在其中一个里{python, rust}对应的英文方法是union()、intersection()、difference()、symmetric_difference()。有个细节值得记运算符要求两边都是集合而方法版接受任意可迭代对象# 适用于 Python 3.8a {a, b, c}print(a {a, z}) # ✅ 两边都是集合可以print(a.intersection([a, z])) # ✅ 方法版能收列表{a}print(a [a, z]) # ❌ TypeError运算符不收列表判断包含关系用子集、超集# 适用于 Python 3.8small {a, b}big {a, b, c}print(small big) # Truesmall 是 big 的子集print(small big) # True真子集print(big small) # True超集五、什么时候用哪个需要「查一个东西对应的值」时用字典只需要「判断某东西在不在」「去重」「做集合运算」时用集合。集合还有一个现实的用处当数据的唯一性靠集合来维护时代码意图比字典更直白。# 适用于 Python 3.8# 场景统计一段文本里出现过哪些不同的词text the quick brown fox jumps over the lazy dog the endwords set(text.split())print(len(words)) # 不同词的数量print(fox in words) # True常见坑点坑一把{}当成空集合。# 适用于 Python 3.8# ❌ 空花括号是字典x {}print(type(x)) # class dict# ✅ 空集合要写 set()x set()坑二往集合里加不可哈希对象。# 适用于 Python 3.8# ❌ 列表不能进集合s set()s.add([1, 2]) # TypeError: unhashable type: list# ✅ 用元组s.add((1, 2))坑三以为集合保留插入顺序。# 适用于 Python 3.8# ❌ set 不保证顺序输出顺序不可依赖s {3, 1, 2}print(list(s)) # 顺序不保证# ✅ 要保留顺序就改用 dicts dict.fromkeys([3, 1, 2])print(list(s)) # [3, 1, 2]坑四用运算符做集合运算却传了列表。# 适用于 Python 3.8# ❌ 运算符两边都必须是集合print({a} [a, b]) # TypeError# ✅ 用方法版或先转成集合print({a}.intersection([a, b])) # {a}print({a} set([a, b])) # {a}坑五想用把两个集合拼起来。# 适用于 Python 3.8# ❌ 集合没有 运算符并集要用 |a {1, 2}b {3}print(a b) # TypeError: unsupported operand type(s) for # ✅ 并集用 | 或 unionprint(a | b) # {1, 2, 3}坑六靠list(set(...))又想要顺序。# 适用于 Python 3.8# ❌ 去重后想要原顺序set 做不到print(list(set([3, 1, 2, 1]))) # 顺序不保证# ✅ 用 dict.fromkeys 保序去重print(list(dict.fromkeys([3, 1, 2, 1]))) # [3, 1, 2]坑七用remove删一个不存在的元素。# 适用于 Python 3.8# ❌ remove 遇到不存在的元素会抛 KeyErrors {a}s.remove(b) # KeyError: b# ✅ 不确定在不在就用 discard它不会报错s.discard(b)print(s) # {a}总结对比项字典 dict集合 set承载内容键值映射纯元素去重键唯一元素唯一顺序3.7 保持插入顺序不保证成员判断键 in d元素 in s集合运算键视图可做原生支持 典型用途查找映射去重、成员判断、集合运算一句话记忆集合是「不要值」的字典。二者共用哈希表这个底座所以都要求元素可哈希、成员判断都快但它们解决的问题不同——一个负责映射一个负责「在不在」和集合运算。