对python的再认识-基于数据结构进行-a008-集合-拓展

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 集合无序
需要索引访问 listdict 集合无序无索引
集合运算 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
相关推荐
孟健10 小时前
Karpathy 用 200 行纯 Python 从零实现 GPT:代码逐行解析
python
码路飞12 小时前
写了个 AI 聊天页面,被 5 种流式格式折腾了一整天 😭
javascript·python
曲幽15 小时前
FastAPI压力测试实战:Locust模拟真实用户并发及优化建议
python·fastapi·web·locust·asyncio·test·uvicorn·workers
敏编程19 小时前
一天一个Python库:jsonschema - JSON 数据验证利器
python
前端付豪19 小时前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain
databook19 小时前
ManimCE v0.20.1 发布:LaTeX 渲染修复与动画稳定性提升
python·动效
花酒锄作田1 天前
使用 pkgutil 实现动态插件系统
python
前端付豪1 天前
LangChain链 写一篇完美推文?用SequencialChain链接不同的组件
人工智能·python·langchain
曲幽2 天前
FastAPI实战:打造本地文生图接口,ollama+diffusers让AI绘画更听话
python·fastapi·web·cors·diffusers·lcm·ollama·dreamshaper8·txt2img
老赵全栈实战2 天前
Pydantic配置管理最佳实践(一)
python