← 返回题目列表

Python set 和 frozenset 有什么区别?集合运算怎么用?

高频 简单 第 5 / 21 题 更新于 2026/07/25
Pythonsetfrozenset集合

简化版

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两边都有
并集`ab`
差集a - b在 a 中不在 b 中
对称差集a ^ b只在其中一边

四、removediscardpopclear 的区别

删除元素时:

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 适合不可变集合语义

frozensetset 的关系有点像 tuplelist

  • 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() 返回某个元素,不应依赖顺序。
  • 追问:removediscard 有什么区别? 元素不存在时 removeKeyErrordiscard 静默无事。
  • 追问:frozenset 为什么能做 dict key? 它不可变且可哈希,前提是内部元素也可哈希。
  • 追问:如何去重并保留原始顺序? 常用 list(dict.fromkeys(items)),因为 dict 保持插入顺序。

八、加强记忆

把 set 记成“哈希集合工具箱”:去重、成员判断、交并差是主场,元素必须可哈希,不负责业务顺序。set 可变,适合增删;frozenset 不可变,适合做 dict key 或嵌套集合元素。删除确定存在用 remove,不确定存在用 discard,需要顺序时显式排序或用 dict 保序去重。