对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 集合无序
需要索引访问 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
相关推荐
夏天拐跑了西瓜1 小时前
一文入门LangChain:从框架认知到构建你的第一个AI Agent
python·langchain·conda·agent
Logic1012 小时前
C语言/数据结构位运算题解:异或XOR找出学习计划中的“独特学习时间“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
魔猴疯猿2 小时前
从0到1用Python开发第一个智能体
人工智能·python·深度学习·神经网络·机器学习
小蒋学算法2 小时前
算法-交替删除操作后最后剩下的整数-跟着灵茶山艾府学算法
数据结构·算法
m0_380743872 小时前
C++中的freopen的用法实例详解
开发语言·c++
xiaoqi01953 小时前
机器学习因子分析实战:从量化特征到可视化决策
人工智能·python·机器学习
Escalating_xu3 小时前
【Python】基础语法(1):常量、变量、类型、输入输出与运算符
开发语言·python
jimy13 小时前
polymorph.cpp里面的“析构函数、智能指针”问题
开发语言·c++
m0_380743873 小时前
PHP 8.3 缓存清除不生效怎么排查
开发语言·php
happylifetree3 小时前
Python14:核心语法-数据存储与运算-字符串定义
python