

大家好,我是正在实战各种AI项目的程序员晚枫。
问你一个问题:你有多少个微信群?这些群里的人,有多少是重叠的?
我大概有100多个群,想统计哪些人是"万人骑"(同时在多个群里)——用列表来算,要写半天。
但用Python的集合(set),只要3行代码搞定。
这就是今天要讲的——被很多人忽视,但其实超级好用的集合。
什么是集合?
先来一个形象的比喻。
想象你面前有一堆混在一起的球,有红的有蓝的有绿的,有的重复了:
现在你把它们倒进一个箱子里,摇一摇——拿出来时,每种颜色的球只出现一次。
这就是集合:会自动去重、没有顺序、只存唯一值。
集合的三大特性
| 特性 | 说明 | 类比 |
|---|
| 唯一性 | 自动去除重复元素 | 去重 |
| 无序性 | 不保证元素顺序(Python 3.7+内部实现有序,但不应依赖) | 抽奖箱 |
| 确定性 | 每个元素必须是不可变(可哈希)的 | 不能放列表进去 |
集合基础:创建与基本操作
3种方式创建集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| s1 = {1, 2, 3, 3, 3} print(s1)
s2 = set([1, 2, 2, 3, 3, 3]) print(s2)
s3 = set("hello") print(s3)
s4 = {x**2 for x in range(1, 6)} print(s4)
|
❌ 必须注意:空集合的坑
1 2 3 4 5 6 7 8 9 10
| empty_dict = {} print(type(empty_dict))
empty_set = set() print(type(empty_set))
print(bool(empty_set))
|
添加和删除元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| s = {1, 2, 3}
s.add(4) print(s)
s.update([5, 6, 7]) print(s)
s.remove(3) print(s)
s.discard(99) s.remove(99)
popped = s.pop() print(f"删除了:{popped}, 剩下:{s}")
s.clear() print(s)
|
最常用功能:一键去重
这是集合最常用的场景,没有之一。
场景:去掉重复的客户名单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| customers = ["张三", "李四", "王五", "张三", "赵六", "李四", "钱七"] print(f"原始列表({len(customers)}人):{customers}")
unique1 = [] for name in customers: if name not in unique1: unique1.append(name)
unique2 = list(dict.fromkeys(customers))
unique3 = list(set(customers))
print(f"去重后({len(unique3)}人):{unique3}")
|
运行结果:
1 2
| 原始列表(7人):['张三', '李四', '王五', '张三', '赵六', '李四', '钱七'] 去重后(5人):['张三', '李四', '王五', '赵六', '钱七']
|
去重 vs 列表:性能对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import time
data = list(range(100000)) + list(range(50000)) print(f"原始数据:{len(data)}条,重复:50000条")
start = time.time() unique1 = [] for item in data: if item not in unique1: unique1.append(item) loop_time = time.time() - start print(f"循环去重耗时:{loop_time:.4f}秒,结果:{len(unique1)}条")
start = time.time() unique2 = list(dict.fromkeys(data)) dict_time = time.time() - start print(f"字典去重耗时:{dict_time:.4f}秒,结果:{len(unique2)}条")
start = time.time() unique3 = list(set(data)) set_time = time.time() - start print(f"集合去重耗时:{set_time:.4f}秒,结果:{len(unique3)}条")
print(f"\n集合比循环快 {loop_time/set_time:.0f} 倍!")
|
实测数据参考:
1 2 3 4 5 6
| 原始数据:150000条,重复:50000条 循环去重耗时:15.2341秒,结果:100000条 字典去重耗时:0.0382秒,结果:100000条 集合去重耗时:0.0121秒,结果:100000条
集合比循环快 1258 倍!
|
去重的注意事项:顺序丢失
1 2 3 4 5 6 7 8
| data = [3, 1, 4, 1, 5, 9, 2, 6, 5] unique = list(set(data)) print(unique)
unique_ordered = list(dict.fromkeys(data)) print(unique_ordered)
|
集合的关系运算(超实用)
这是集合最强大的功能——关系运算。处理"谁和谁有交集""哪些是共有的"这类问题,特别方便。
场景:分析两个班级的共同好友
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| alice_friends = {'Bob', 'Charlie', 'David', 'Eve'}
bob_friends = {'Alice', 'Charlie', 'Eve', 'Frank'}
common = alice_friends & bob_friends print(f"共同好友:{common}")
all_friends = alice_friends | bob_friends print(f"所有好友:{all_friends}")
only_alice = alice_friends - bob_friends print(f"只在 Alice 列表:{only_alice}")
not_common = alice_friends ^ bob_friends print(f"非共同好友:{not_common}")
|
图解:
1 2 3 4 5 6 7
| Alice: {Bob, Charlie, David, Eve} Bob: {Alice, Charlie, Eve, Frank}
交集 & = {Charlie, Eve} ← 共同好友 并集 | = {Alice, Bob, Charlie, David, Eve, Frank} ← 所有人 差集 Alice - Bob = {Bob, David} ← Alice有但Bob没有 对称差集 ^ = {Alice, Bob, David, Frank} ← 不是共同好友的
|
运算符 vs 方法 对照表
| 运算符 | 方法 | 含义 | 示例 |
|---|
& | intersection() | 交集(共同元素) | {1,2} & {2,3} = {2} |
| | union() | 并集(所有元素) | {1,2} | {2,3} = {1,2,3} |
- | difference() | 差集(在A不在B) | {1,2} - {2,3} = {1} |
^ | symmetric_difference() | 对称差集(不共有) | {1,2} ^ {2,3} = {1,3} |
更多关系判断方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| a = {1, 2, 3} b = {1, 2} c = {4, 5}
print(b.issubset(a)) print(a.issuperset(b))
print(a.isdisjoint(c))
print(b <= a) print(b < a) print(a >= b) print(a > b) print(a.isdisjoint(c))
|
实战案例:销售数据分析
案例1:分析两个文件中的客户重叠情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| file_a = {1001, 1002, 1003, 1004, 1005}
file_b = {1004, 1005, 1006, 1007, 1008}
both_files = file_a & file_b print(f"两个文件都有的客户:{both_files}") print(f"人数:{len(both_files)}人")
only_a = file_a - file_b print(f"只在A文件的客户:{only_a}")
only_b = file_b - file_a print(f"只在B文件的客户:{only_b}")
total = len(file_a | file_b) print(f"总客户数(去重后):{total}人")
overlap_ratio = len(both_files) / len(file_a | file_b) * 100 print(f"重叠率:{overlap_ratio:.1f}%")
|
案例2:微信群"万人骑"分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| group_tech = {'张三', '李四', '王五', '赵六', '程序员晚枫'} group_python = {'李四', '王五', '孙七', '程序员晚枫'} group_ai = {'张三', '程序员晚枫', '周八', '吴九'} group_startup = {'李四', '赵六', '郑十', '程序员晚枫'} group_book = {'王五', '孙七', '程序员晚枫'}
all_groups = [group_tech, group_python, group_ai, group_startup, group_book]
in_all = set.intersection(*all_groups) print(f"在所有群里的:{in_all}")
all_members = set.union(*all_groups) print(f"总人数(去重):{len(all_members)}人")
cnt_wanfeng = sum(1 for g in all_groups if '程序员晚枫' in g) print(f"程序员晚枫在 {cnt_wanfeng} 个群里")
def get_member_count(member): return sum(1 for g in all_groups if member in g)
for member in all_members: cnt = get_member_count(member) if cnt >= 3: print(f" {member} 在 {cnt} 个群里")
|
案例3:文章关键词去重与统计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| article1_keywords = {'Python', '编程', '入门', '教程', '基础'} article2_keywords = {'Python', '进阶', '技巧', '高级', '编程'} article3_keywords = {'Python', '项目', '实战', '入门', '案例'}
all_keywords = article1_keywords | article2_keywords | article3_keywords print(f"所有关键词(去重):{all_keywords}") print(f"共 {len(all_keywords)} 个")
common_keywords = article1_keywords & article2_keywords & article3_keywords print(f"三篇都有:{common_keywords}")
only_article1 = article1_keywords - (article2_keywords | article3_keywords) only_article2 = article2_keywords - (article1_keywords | article3_keywords) only_article3 = article3_keywords - (article1_keywords | article2_keywords) print(f"文章1独有:{only_article1}") print(f"文章2独有:{only_article2}") print(f"文章3独有:{only_article3}")
|
frozenset:不可变的集合
普通集合可以增删,但有时候你需要一个不能修改的集合——比如作为字典的键,或者放进另一个集合里。
frozenset vs set
1 2 3 4 5 6 7 8 9 10 11 12
| s = {1, 2, 3} s.add(4) s.remove(1) print(s)
fs = frozenset([1, 2, 3])
print(fs)
|
frozenset 的应用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| category_mapping = { frozenset(['猫', '狗']): '哺乳动物', frozenset(['鱼']): '鱼类', frozenset(['鸟']): '鸟类', }
print(category_mapping[frozenset(['猫', '狗'])])
s1 = {1, 2} s2 = {3, 4} s_of_sets = {frozenset([1, 2]), frozenset([3, 4])} print(s_of_sets)
def process_tags(tags): for tag in tags: print(f"处理标签:{tag}")
user_tags = frozenset(['Python', 'AI', '编程']) process_tags(user_tags)
|
避坑指南:集合最容易踩的6个坑
坑1:集合只能存不可变对象
1 2 3 4 5 6 7 8 9 10 11 12 13
|
s = {(1, 2, 3)} print(s)
s = {frozenset([1, 2]), frozenset([3, 4])} print(s)
|
坑2:集合的元素必须是唯一的——但会自动去重!
1 2 3 4 5 6 7 8 9 10 11 12
| s = {1, 2, 2, 2, 3, 3, 3} print(s)
s1 = {1, "1", 1.0} print(s1)
s = {1, 2, 3} s.add(1) print(s)
|
坑3:集合遍历的顺序可能不是你想要的
1 2 3 4 5 6 7 8
| s = {'d', 'a', 'c', 'b', 'e'} print(list(s))
data = [3, 1, 4, 1, 5, 9, 2, 6] unique_ordered = list(dict.fromkeys(data)) print(unique_ordered)
|
坑4:差集运算是有方向的!
1 2 3 4 5 6 7 8 9
| a = {1, 2, 3, 4} b = {3, 4, 5, 6}
print(a - b) print(b - a)
print(a ^ b)
|
坑5:交集判断用 &,不是 and
1 2 3 4 5 6 7 8 9 10 11 12 13
| a = {1, 2, 3} b = {2, 3, 4}
if a & b: print(f"有共同元素:{a & b}")
if not a.isdisjoint(b): print(f"有共同元素:{a & b}")
|
坑6:集合的 + 运算是无效的!
1 2 3 4 5 6 7 8 9 10 11 12 13
| a = {1, 2, 3} b = {4, 5, 6}
c = a | b print(c)
combined = set().union(*[a, b, {7, 8}]) print(combined)
|
性能对比:集合 vs 列表 vs 字典
成员判断:集合最快
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import time
n = 10000 data_list = list(range(n)) data_set = set(data_list) data_dict = {x: True for x in range(n)} target = n // 2
start = time.time() for _ in range(1000): _ = target in data_list print(f"列表查找1000次:{time.time()-start:.4f}秒")
start = time.time() for _ in range(1000): _ = target in data_set print(f"集合查找1000次:{time.time()-start:.4f}秒")
start = time.time() for _ in range(1000): _ = target in data_dict print(f"字典查找1000次:{time.time()-start:.4f}秒")
|
结论:集合和字典的成员判断几乎一样快(O(1)),都比列表(O(n))快很多。
内存占用对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import sys
n = 1000
data_list = list(range(n))
data_dict = {x: x for x in range(n)}
data_set = set(range(n))
print(f"列表内存:{sys.getsizeof(data_list)} 字节") print(f"字典内存:{sys.getsizeof(data_dict)} 字节") print(f"集合内存:{sys.getsizeof(data_set)} 字节")
|
结论:只存"是否存在"用集合,比字典省一半内存!
什么时候用集合?
✅ 适合用集合的场景
| 场景 | 原因 |
|---|
| 需要去重 | 一行 list(set(data)) 搞定 |
| 判断成员是否存在 | 比列表快100倍!O(1) vs O(n) |
| 求交集、并集、差集 | 直接用运算符,简洁高效 |
| 需要确保元素唯一性 | 自动去重,永不重复 |
| 比较两组数据差异 | 对称差集 ^ 一行搞定 |
❌ 不适合用集合的场景
| 场景 | 替代方案 |
|---|
| 需要保持顺序 | 用列表,或 dict.fromkeys() |
| 需要通过索引访问 | 用列表 |
| 需要存储重复元素 | 用列表 |
| 需要存可变对象 | 用列表,或用 frozenset 存元组 |
常见面试题
Q1:集合和列表有什么区别?
A:
- 列表:有序、可重复、可修改、有索引 → 适合需要顺序和重复的场景
- 集合:无序、自动去重、可修改、无索引 → 适合去重和关系运算
Q2:如何保持去重后的顺序?
A:用 dict.fromkeys() 或者配合列表:
1 2
| data = [3, 1, 4, 1, 5, 9, 2, 6] unique = list(dict.fromkeys(data))
|
Q3:* 运算符可以重复集合吗?
A:不能! * 是序列(列表/元组)的重复运算符,不是集合的。
1 2 3 4 5 6 7 8 9
|
s = {1, 2} result = set() for _ in range(2): result.update(s) print(result)
|
Q4:集合的元素为什么必须是可哈希的?
A:集合底层用哈希表存储,需要计算元素的哈希值来确定存储位置。可变对象(如列表)的哈希值不稳定,所以不能放。
本讲小结
| 操作 | 代码 | 说明 |
|---|
| 创建集合 | {1, 2, 3} 或 set([1,2,3]) | 花括号或set函数 |
| 空集合 | set() | 注意不是 {} |
| 去重 | list(set(data)) | 一行代码搞定 |
| 交集 | a & b 或 a.intersection(b) | 共同的元素 |
| 并集 | a | b 或 a.union(b) | 所有元素合并 |
| 差集 | a - b 或 a.difference(b) | 在a里不在b里 |
| 对称差集 | a ^ b | 不共有的元素 |
| 子集判断 | a <= b 或 a.issubset(b) | 是否是子集 |
| 不可变集合 | frozenset() | 不能增删 |
💡 记住一句话:要去重用 set!要快判断用 set!要做关系运算还是用 set!
下节预告
学会了集合,下一篇来学装饰器——给函数加功能的"黑魔法"。
👉 继续阅读:Python装饰器
课程导航
上一篇: Python函数参数*args和**kwargs详解
下一篇: Python装饰器:给函数加功能的黑魔法
PS:集合是Python最被低估的数据结构之一。记住:要去重用set,要快用set,要做关系运算还是用set!
🎓 AI 编程实战课程
想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!