Python set 和 frozenset 有什么区别?集合运算怎么用?
简化版
set 是可变集合,元素不重复,适合去重、成员判断和交并差运算;frozenset 是不可变集合,可以作为 dict key 或另一个 set 的元素。集合元素必须可哈希,所以 list、dict、set 不能直接放进 set。
详细版
基本用法:
nums = [1, 2, 2, 3]
unique = set(nums)
print(unique) # {1, 2, 3}
集合运算:
a = {"python", "java", "go"}
b = {"python", "rust"}
print(a & b) # 交集 {'python'}
print(a | b) # 并集
print(a - b) # 差集
print(a ^ b) # 对称差集
set 可变:
s = {1, 2}
s.add(3)
s.remove(1)
frozenset 不可变:
fs = frozenset([1, 2, 3])
# fs.add(4) # AttributeError
因为 frozenset 哈希稳定,所以可以作为 key:
d = {frozenset(["read", "write"]): "admin"}
完整版教学
一、set 的核心价值是去重和成员判断
去重:
ids = [1, 2, 2, 3, 3]
unique_ids = set(ids)
成员判断:
allowed = {1001, 1002, 1003}
if user_id in allowed:
...
当候选集合很大时,用 set 做成员判断通常比 list 更合适,因为 list 要从头扫描,set 可以利用哈希结构快速定位。
数字例子:如果黑名单有 100000 个用户 id,用 list 判断某个 id 是否存在,最坏要比较 100000 次;用 set 平均情况下能通过哈希快速定位。代价是 set 需要额外哈希表空间,而且元素必须可哈希。小数据量下差异不一定重要,但面试要能说出“成员判断和去重”是 set 的主战场。
二、集合不保证按业务顺序使用
很多人把 set 打印出来看到某个顺序,就误以为它有稳定业务顺序。
s = {"b", "a", "c"}
print(s)
不要依赖 set 的展示顺序。如果需要顺序:
sorted(s)
如果需要“去重并保留原始顺序”,可以用 dict:
items = ["b", "a", "b", "c"]
unique_ordered = list(dict.fromkeys(items))
print(unique_ordered) # ['b', 'a', 'c']
set 的顺序不应该承载业务含义。即使某次运行输出看起来“刚好有序”,也不要依赖它;哈希随机化、插入删除过程、解释器版本都可能影响展示。需要排序就用 sorted(s),需要保留原始去重顺序就用 dict.fromkeys。
易错点:set 负责“唯一性”和“集合关系”,不负责“业务顺序”;顺序需求要显式交给 list、dict 或排序逻辑。
三、集合运算是面试高频
交集:两个集合都有。
a & b
a.intersection(b)
并集:合并全部元素。
a | b
a.union(b)
差集:在 a 中但不在 b 中。
a - b
a.difference(b)
对称差集:只在其中一个集合中出现。
a ^ b
a.symmetric_difference(b)
这些操作常用于权限、标签、推荐、黑白名单、共同好友等场景。
可以用权限例子记:
required = {"read", "write"}
actual = {"read", "delete"}
print(required & actual) # {'read'},已有权限
print(required - actual) # {'write'},缺少权限
print(actual - required) # {'delete'},多余权限
print(required | actual) # {'read', 'write', 'delete'},合并
| 运算 | 符号 | 含义 |
|---|---|---|
| 交集 | a & b | 两边都有 |
| 并集 | `a | b` |
| 差集 | a - b | 在 a 中不在 b 中 |
| 对称差集 | a ^ b | 只在其中一边 |
四、remove、discard、pop、clear 的区别
删除元素时:
s = {1, 2, 3}
s.remove(2)
如果元素不存在,remove 会抛 KeyError:
# s.remove(99) # KeyError
discard 不会:
s.discard(99)
所以不确定元素是否存在时,discard 更省心。pop() 会删除并返回集合中的某个元素,但因为 set 没有业务顺序,不要把它理解成“弹出第一个”或“弹出最后一个”。clear() 则清空整个集合。
s = {1, 2, 3}
value = s.pop() # 返回某个元素,不保证业务顺序
s.clear()
print(s) # set()
五、frozenset 适合不可变集合语义
frozenset 和 set 的关系有点像 tuple 和 list:
set可变,不可哈希;frozenset不可变,可哈希,前提是内部元素可哈希。
例如权限组合:
role_map = {
frozenset(["read"]): "viewer",
frozenset(["read", "write"]): "editor",
}
这里权限集合本身可以作为 key,因为它不可变。
frozenset 的价值是“集合语义 + 可哈希”。普通 set 不能放进另一个 set,因为它可变;frozenset 可以。比如你要保存多个无序组合,每个组合内部不关心顺序,就可以用 set[frozenset]。
pairs = {
frozenset({"Tom", "Jerry"}),
frozenset({"Alice", "Bob"}),
}
注意,frozenset 内部元素仍然必须可哈希。frozenset([[1], [2]]) 仍然不行,因为里面是 list。
六、set 推导式和可变性边界
set 推导式常用于从数据流里提取唯一值:
users = [{"city": "Beijing"}, {"city": "Shanghai"}, {"city": "Beijing"}]
cities = {user["city"] for user in users}
print(cities) # {'Beijing', 'Shanghai'}
如果元素本身是 dict/list 这种不可哈希对象,不能直接放入 set,要提取可哈希字段或转换成 tuple/frozenset。转换时要确认语义:列表转 tuple 会保留顺序,set 转 frozenset 不关心顺序。不要为了“能 hash”盲目转换,导致业务含义变了。
七、常见误区与追问
- 误区:set 会自动排序。 set 不承诺业务顺序,排序要用
sorted,保留原顺序去重要用dict.fromkeys等方式。 - 误区:set 里面可以放任何对象。 set 元素必须可哈希,list、dict、set 不能直接作为元素。
- 误区:
pop()会弹出固定位置元素。 set 没有业务顺序,pop()返回某个元素,不应依赖顺序。 - 追问:
remove和discard有什么区别? 元素不存在时remove抛KeyError,discard静默无事。 - 追问:frozenset 为什么能做 dict key? 它不可变且可哈希,前提是内部元素也可哈希。
- 追问:如何去重并保留原始顺序? 常用
list(dict.fromkeys(items)),因为 dict 保持插入顺序。
八、加强记忆
把 set 记成“哈希集合工具箱”:去重、成员判断、交并差是主场,元素必须可哈希,不负责业务顺序。set 可变,适合增删;frozenset 不可变,适合做 dict key 或嵌套集合元素。删除确定存在用 remove,不确定存在用 discard,需要顺序时显式排序或用 dict 保序去重。