Python 集合拓展
本文聚焦 Python 集合的高级用法,包含 frozenset、常见陷阱、性能对比、实际应用场景及最佳实践。
简单导图

一、frozenset(不可变集合)
1.1 什么是 frozenset
frozenset:不可变的集合,一旦创建无法修改,可哈希,可作为字典键或集合元素。
python
# 创建 frozenset
fs = frozenset([1, 2, 3, 2])
# frozenset({1, 2, 3}) - 自动去重
# 创建方式
fs1 = frozenset([1, 2, 3]) # 从列表
fs2 = frozenset({1, 2, 3}) # 从集合
fs3 = frozenset((1, 2, 3)) # 从元组
fs4 = frozenset("hello") # 从字符串
fs5 = frozenset() # 空集合
# 转换为 frozenset
s = {1, 2, 3}
fs = frozenset(s)
1.2 frozenset vs set
python
# set:可变,不可哈希
s = {1, 2, 3}
s.add(4) # ✅ 可以修改
hash(s) # ❌ TypeError: unhashable type: 'set'
# frozenset:不可变,可哈希
fs = frozenset({1, 2, 3})
fs.add(4) # ❌ AttributeError: 'frozenset' object has no attribute 'add'
hash(fs) # ✅ 返回哈希值
1.3 frozenset 支持的操作
python
fs = frozenset([1, 2, 3, 4, 5])
# ✅ 支持的只读操作
len(fs) # 5
3 in fs # True
for x in fs: # 可遍历
print(x)
# ✅ 支持集合运算
fs_a = frozenset([1, 2, 3])
fs_b = frozenset([3, 4, 5])
fs_a | fs_b # frozenset({1, 2, 3, 4, 5}) - 并集
fs_a & fs_b # frozenset({3}) - 交集
fs_a - fs_b # frozenset({1, 2}) - 差集
fs_a ^ fs_b # frozenset({1, 2, 4, 5}) - 对称差
# ✅ 关系判断
fs_a.issubset(fs_b) # False
fs_a.issuperset({1, 2}) # True
fs_a.isdisjoint({4, 5, 6}) # False
# ✅ 复制
fs_copy = fs.copy()
1.4 frozenset 作为字典键
python
# ✅ frozenset 可作字典键
d = {
frozenset({1, 2, 3}): "first",
frozenset({4, 5}): "second"
}
d[frozenset({1, 2, 3})] # "first"
# ❌ set 不可作字典键
# d = {{1, 2, 3}: "value"} # TypeError
1.5 frozenset 作为集合元素
python
# ✅ frozenset 可作为集合元素
s = {
frozenset({1, 2}),
frozenset({3, 4}),
frozenset({5, 6})
}
# {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}
frozenset({1, 2}) in s # True
# ❌ set 不可作为集合元素
# s = {{1, 2}, {3, 4}} # TypeError
1.6 frozenset 实际应用
python
# 1. 缓存函数结果(参数包含集合)
from functools import lru_cache
def process_data(tags):
# tags 是集合,但不可哈希,无法作为缓存键
return expensive_operation(tags)
# ✅ 解决:使用 frozenset
@lru_cache(maxsize=None)
def process_data_cached(tags):
# tags 是 frozenset,可哈希
return expensive_operation(tags)
process_data_cached(frozenset({"python", "ai"}))
# 2. 表示无序的固定配置
ALLOWED_ORIGINS = frozenset({
"https://example.com",
"https://api.example.com"
})
# 不能被意外修改
# 3. 图论中的边集
edges = {
frozenset({1, 2}), # 边连接 1 和 2
frozenset({2, 3}), # 边连接 2 和 3
frozenset({1, 3}), # 边连接 1 和 3
}
# 无序的边,(1,2) 和 (2,1) 是同一条边
# 4. 集合的集合
matrix_rows = {
frozenset({1, 2, 3}),
frozenset({2, 3, 4}),
frozenset({3, 4, 5})
}
二、集合推导式
2.1 基础语法
python
# 集合推导式(自动去重)
numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
# 直接收集,自动去重
s = {x for x in numbers}
# {1, 2, 3, 4}
# 对比:原列表有 10 个元素(包含重复)
# 集合只有 4 个元素(自动去重)
# 带条件的推导
evens = {x for x in numbers if x % 2 == 0}
# {2, 4} - 只保留偶数,同时去重
# 嵌套推导(扁平化二维数据)
matrix = [[1, 2], [3, 4], [5, 6]]
flat = {num for row in matrix for num in row}
# {1, 2, 3, 4, 5, 6}
2.2 集合推导式 vs 其他方式
python
# 方法 1:集合推导式(推荐)
s1 = {x for x in [1, 2, 2, 3, 3]}
# {1, 2, 3}
# 方法 2:set() + 生成器
s2 = set(x for x in [1, 2, 2, 3, 3])
# {1, 2, 3}
# 方法 3:先列表再转集合(多一步)
s3 = set([x for x in [1, 2, 2, 3, 3]])
# {1, 2, 3}
# 方法 4:直接从列表转集合
s4 = set([1, 2, 2, 3, 3])
# {1, 2, 3}
# 推荐:方法 1 最简洁,方法 4 最简单
2.3 实际应用
python
# 1. 提取唯一属性
users = [
{"name": "Alice", "city": "NYC"},
{"name": "Bob", "city": "LA"},
{"name": "Charlie", "city": "NYC"}
]
cities = {user["city"] for user in users}
# {"NYC", "LA"}
# 2. 文本处理
text = "hello world"
chars = {c for c in text if c.isalpha()}
# {'h', 'e', 'l', 'o', 'w', 'r', 'd'}
# 3. 坐标去重
points = [(1, 2), (3, 4), (1, 2), (5, 6)]
unique_points = {point for point in points}
# {(1, 2), (3, 4), (5, 6)}
三、常见陷阱
3.1 空集合语法
python
# ❌ 错误:{} 创建空字典,不是空集合
empty = {}
type(empty) # <class 'dict'>
# ✅ 正确:使用 set()
empty = set()
type(empty) # <class 'set'>
3.2 元素必须可哈希
python
# ❌ 错误:列表不可哈希
# s = {[1, 2], [3, 4]} # TypeError: unhashable type: 'list'
# ✅ 解决:使用元组
s = {(1, 2), (3, 4)} # 有效
# ❌ 错误:集合不可哈希(嵌套集合)
# s = {{1, 2}, {3, 4}} # TypeError
# ✅ 解决:使用 frozenset
s = {frozenset({1, 2}), frozenset({3, 4})}
# {frozenset({1, 2}), frozenset({3, 4})}
# ❌ 错误:字典不可哈希
# s = {{"a": 1}, {"b": 2}} # TypeError
# 注意:如果需要存储键值对集合,应使用元组
s = {("a", 1), ("b", 2)} # {( 'a', 1), ('b', 2)}
3.3 集合无序
python
s = {3, 1, 4, 1, 5, 9, 2, 6}
# ❌ 不要依赖集合的顺序
# print(s[0]) # TypeError: 'set' object is not subscriptable
# ❌ 不要假设顺序
for x in s:
print(x) # 输出顺序不确定
# ✅ 需要有序时转为列表
sorted_list = sorted(s) # [1, 2, 3, 4, 5, 6, 9]
3.4 修改正在遍历的集合
python
s = {1, 2, 3, 4, 5}
# ❌ 错误:遍历时修改集合大小
# for x in s:
# if x % 2 == 0:
# s.remove(x) # RuntimeError: Set changed size during iteration
# ✅ 解决 1:遍历副本
for x in s.copy():
if x % 2 == 0:
s.remove(x)
# {1, 3, 5}
# ✅ 解决 2:使用集合推导式创建新集合
s = {x for x in s if x % 2 != 0}
# {1, 3, 5}
# ✅ 解决 3:原地过滤
s.intersection_update({1, 3, 5, 7, 9})
3.5 运算符优先级
python
a = {1, 2}
b = {2, 3}
c = {3, 4}
# ❌ 可能的错误理解
# result = a & b | c # 实际是 (a & b) | c = {2, 3, 4}
# ✅ 明确使用括号
result = a & (b | c) # {2}
result = (a & b) | c # {2, 3, 4}
3.6 混淆 = 与 ==
python
s = {1, 2, 3}
# == 比较内容(元素相同即为 True)
s == {3, 2, 1} # True
s == {1, 2, 3, 4} # False
# is 比较身份(是否同一对象)
s2 = {1, 2, 3}
s is s2 # False(不同对象)
s is s # True(同一对象)
# 不要用 is 比较集合内容
if s == {1, 2, 3}: # ✅ 正确
print("相等")
if s is {1, 2, 3}: # ❌ 错误(永远 False)
print("相等")
四、性能对比
4.1 成员检测:set vs list
python
import timeit
# 大数据量测试
data = list(range(100000))
search_set = set(data)
# 列表成员检测:O(n)
time_list = timeit.timeit('99999 in data', setup='data=list(range(100000))', number=10000)
# ~2.5s
# 集合成员检测:O(1)
time_set = timeit.timeit('99999 in search_set', setup='search_set=set(range(100000))', number=10000)
# ~0.0001s
print(f"列表: {time_list:.4f}s")
print(f"集合: {time_set:.4f}s")
print(f"集合快 {time_list/time_set:.0f} 倍!")
结论 :成员检测场景,集合比列表快 25000 倍!
4.2 去重性能
python
# 方法 1:列表手动去重(低效)
def unique_list(lst):
result = []
for item in lst:
if item not in result: # O(n) 每次检测
result.append(item)
return result
# 时间复杂度:O(n²)
# 方法 2:使用集合(高效)
def unique_set(lst):
return list(set(lst))
# 时间复杂度:O(n)
# 性能测试
data = list(range(10000)) * 10 # 100000 个元素,有重复
import timeit
time_list = timeit.timeit('unique_list(data)', setup='from __main__ import unique_list, data', number=10)
# ~15s
time_set = timeit.timeit('unique_set(data)', setup='from __main__ import unique_set, data', number=10)
# ~0.01s
print(f"列表去重: {time_list:.4f}s")
print(f"集合去重: {time_set:.4f}s")
print(f"集合快 {time_list/time_set:.0f} 倍!")
4.3 内存占用
python
import sys
# 集合 vs 列表内存
lst = list(range(100000))
s = set(range(100000))
print(f"列表内存: {sys.getsizeof(lst):,} 字节")
print(f"集合内存: {sys.getsizeof(s):,} 字节")
# 集合通常占用更多内存(哈希表结构)
五、实际应用场景
5.1 去重
python
# 列表去重
nums = [1, 2, 2, 3, 3, 3, 4]
unique = list(set(nums))
# [1, 2, 3, 4] - 顺序可能改变
# 保持顺序的去重
def unique_ordered(lst):
seen = set()
return [x for x in lst if not (x in seen or seen.add(x))]
unique = unique_ordered(nums)
# [1, 2, 3, 4] - 保持原顺序
# 字符串去重
s = "aabbccdd"
''.join(set(s))
# 'abcd' - 顺序可能改变
# 字典按键去重(Python 3.7+ 字典保持插入顺序)
pairs = [('a', 1), ('b', 2), ('a', 3), ('c', 4)]
unique = dict(pairs)
# {'a': 3, 'b': 2, 'c': 4}
5.2 标签系统
python
# 用户兴趣标签
user_a = {"python", "coding", "ai", "data"}
user_b = {"python", "web", "database"}
# 找共同兴趣(交集)
common = user_a & user_b
# {'python'}
# 推荐标签(用户 b 有,用户 a 没有)
recommend = user_b - user_a
# {'web', 'database'}
# 合并标签(并集)
all_tags = user_a | user_b
# {'python', 'coding', 'ai', 'data', 'web', 'database'}
# 找独特标签(对称差)
unique = user_a ^ user_b
# {'coding', 'ai', 'data', 'web', 'database'}
5.3 权限检查
python
# 角色权限
admin = {"read", "write", "delete", "manage"}
editor = {"read", "write"}
viewer = {"read"}
# 检查权限
def has_permission(user_role, permission):
return permission in user_role
has_permission(admin, "delete") # True
has_permission(editor, "delete") # False
# 检查多个权限(是否包含所有权限)
required = {"read", "write"}
required.issubset(editor) # True
required.issubset(viewer) # False
# 检查是否有任一权限
allowed = {"read", "write"}
allowed.intersection(viewer) # {'read'} - 非空表示有权限
5.4 数据对比
python
# 两个版本的差异
old_version = {"a", "b", "c", "d"}
new_version = {"b", "c", "e", "f"}
# 新增的
added = new_version - old_version
# {'e', 'f'}
# 删除的
removed = old_version - new_version
# {'a', 'd'}
# 不变的
unchanged = old_version & new_version
# {'b', 'c'}
# 所有变化的
changed = old_version ^ new_version
# {'a', 'd', 'e', 'f'}
5.5 爬虫去重
python
# 已访问的 URL
visited_urls = set()
def crawl(url):
if url in visited_urls:
print(f"已访问:{url}")
return
# 访问 URL
print(f"访问中:{url}")
visited_urls.add(url)
# 模拟抓取新链接
new_links = [f"{url}/{i}" for i in range(3)]
for link in new_links:
crawl(link)
crawl("https://example.com")
5.6 数据分组与聚合
python
# 按条件分组
from collections import defaultdict
data = [
("A", 1), ("B", 2), ("A", 3), ("C", 4),
("B", 5), ("A", 6), ("C", 7)
]
# 方法 1:使用 defaultdict
groups = defaultdict(list)
for key, value in data:
groups[key].append(value)
# {'A': [1, 3, 6], 'B': [2, 5], 'C': [4, 7]}
# 方法 2:找出所有唯一键
keys = {key for key, _ in data}
# {'A', 'B', 'C'}
六、最佳实践
6.1 使用场景选择
| 场景 | 推荐数据结构 | 原因 |
|---|---|---|
| 需要去重 | set |
自动去重 |
| 频繁成员检测 | set |
O(1) 查找 |
| 需要保持顺序 | list |
集合无序 |
| 需要索引访问 | list 或 dict |
集合无序无索引 |
| 集合运算 | set |
原生支持交并差 |
| 存储可变数据 | list |
集合元素必须可哈希 |
| 固定集合数据 | frozenset |
不可变,可哈希 |
| 集合作为键 | frozenset |
set 不可哈希 |
6.2 代码风格
python
# ✅ 好的实践
# 1. 去重用集合
unique = list(set(duplicate_list))
# 2. 成员检测用集合
VALID_VALUES = {1, 2, 3, 4, 5}
if value in VALID_VALUES: # O(1)
pass
# 3. 集合运算用运算符(更简洁)
result = set_a & set_b # 而非 set_a.intersection(set_b)
# 4. 批量操作用 update
large_set.update(new_elements) # 而非循环 add
# 5. 空集合用 set()
s = set() # 而非 {}
# 6. 需要固定集合用 frozenset
CONFIG = frozenset({"opt1", "opt2", "opt3"})
# ❌ 避免
# 1. 只有一个元素时用集合(除非需要去重)
single = {1} # 可以,但如果是常量考虑用变量
# 2. 需要顺序时用集合
# ordered = set(data) # 顺序不确定
# 3. 频繁增删单个元素且关心顺序
# 应该用列表
# 4. 使用 is 比较集合内容
# if s is {1, 2, 3}: # 错误
# 5. 遍历时直接修改集合
# for x in s:
# s.remove(x) # 错误
6.3 性能优化
python
# ✅ 优化 1:预先创建集合
# 不推荐
if value in [1, 2, 3, 4, 5]: # O(n)
pass
# 推荐
VALID_VALUES = {1, 2, 3, 4, 5}
if value in VALID_VALUES: # O(1)
pass
# ✅ 优化 2:批量操作
# 不推荐
for item in large_list:
s.add(item) # 多次调用
# 推荐
s.update(large_list) # 一次调用
# ✅ 优化 3:使用集合运算代替循环
# 不推荐
result = []
for x in set_a:
if x in set_b:
result.append(x)
# 推荐
result = set_a & set_b
# ✅ 优化 4:选择正确的数据结构
# 需要频繁成员检测 → 用集合
items = set(range(10000))
if 5000 in items: # O(1)
pass
# 只需要遍历一次 → 用生成器
sum(x for x in range(10000)) # 节省内存
七、总结
| 主题 | 要点 |
|---|---|
| frozenset | 不可变集合,可哈希,可作字典键和集合元素 |
| 集合推导式 | {x for x in ...} 语法,自动去重 |
| 空集合 | 必须用 set(),{} 是空字典 |
| 元素要求 | 元素必须可哈希(不可变) |
| 无序性 | 集合无序,不依赖顺序 |
| 遍历修改 | 不能在遍历时修改集合大小 |
| 成员检测 | O(1) 性能,远优于列表 |
| 去重 | list(set(lst)) 高效去重 |
| 集合运算 | & ` |
| frozenset 应用 | 缓存键、固定配置、图论边集 |
| 最佳实践 | 去重用 set,成员检测用 set,需要顺序用 list |