第4章 4.4 集合 set 与去重
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了字典这位"超级管家",它能存储键值对、帮我们用名字查信息,简直是数据管理的神器。
但你有没有遇到过这种情况:
场景一:你的程序从数据库里查出了 1000 条订单记录,其中有些是重复的(比如用户刷新了页面),你想去掉重复的怎么办?
场景二:你有两个名单,A 班同学和 B 班同学,现在想知道哪些同学同时选了 A 和 B 的课?哪些只在 A 班?
如果用列表来做,你会发现:去重要写循环、查交集要嵌套循环、代码又臭又长……
集合(set) 就是来解决这个问题的!它天生就会去重,天生就擅长做"有没有""交并差"这类运算。学会它,你的代码能少写一半。
学完这章,你就能:
- 用集合给数据去重,速度比列表快 10 倍
- 轻松做交集、并集、差集运算
- 用 frozenset 保护你的数据不被意外修改
🧱 基础 25 分钟:核心概念
什么是集合?—— 水果篮子的故事
想象你有一个水果篮子:
- 每个位置只能放一种水果
- 不能放两个一样的苹果
- 你可以随时往里加水果,或者看看篮子里有没有西瓜
集合(set) 就是这个水果篮子:
- 无序:水果在里面不按顺序排(没有 index)
- 去重:自动去掉重复的水果
- 可变:可以随时添加删除元素
# 定义一个集合
fruits = {"苹果", "香蕉", "橙子", "苹果"} # 注意:有两个苹果
print(fruits) # 输出:{'香蕉', '橙子', '苹果'} # 自动去重了!
print(len(fruits)) # 输出:3 # 只有3种水果
一句话解释:{"苹果", "香蕉"} 这叫集合字面量,里面放什么都行,但每样只能有一个。
为什么不用列表去重?
你说用列表也能去重啊,我遍历一遍就行了。来看对比:
# 用列表去重(慢)
names = ["张三", "李四", "王五", "张三", "赵六", "李四"]
unique_names = []
for name in names:
if name not in unique_names: # 每次都要遍历整个列表
unique_names.append(name)
print(unique_names) # ['张三', '李四', '王五', '赵六']
# 用集合去重(快)
unique_names = list(set(names)) # 一行搞定!
print(unique_names) # ['张三', '李四', '王五', '赵六']
区别在哪?
| 方式 | 原理 | 1000 条数据耗时 |
|---|---|---|
| 列表 | 逐个比较,O(n²) | 慢 |
| 集合 | 哈希查找,O(1) | 快 |
一句话解释:in 判断在列表里是"挨家挨户敲门",在集合里是"直接查户口本"。

集合的增删改查
集合是可变的,可以动态操作:
# 创建空集合(注意:不能用 {},那是字典!)
empty_set = set() # 正确
not_empty = {"apple"} # 这才是有内容的集合
# 添加元素
colors = {"红色", "蓝色"}
colors.add("绿色") # 添加一个
print(colors) # {'红色', '蓝色', '绿色'}
# 添加多个
colors.update(["黄色", "紫色"]) # 一次加一堆
print(colors) # {'红色', '蓝色', '绿色', '黄色', '紫色'}
# 删除元素(不存在会报错)
colors.remove("蓝色") # 删除指定元素
print(colors) # {'红色', '绿色', '黄色', '紫色'}
# 删除元素(不存在不报错)
colors.discard("白色") # 删不存在的元素也不报错
print(colors) # 保持原样
# 随机弹出一个(集合无序,所以是"随机")
popped = colors.pop()
print(f"弹出了:{popped}")
print(f"剩下:{colors}")
# 清空
colors.clear()
print(colors) # set()
一句话解释:添加用 add(一次一个)或 update(一次一群),删除用 remove(会报错)或 discard(不报错)。
查成员——有没有?
这是集合最常用的操作,问"这个东西在不在里面":
students = {"小明", "小红", "小刚", "小丽"}
# 判断是否存在(返回 True/False)
print("小明" in students) # True
print("小华" in students) # False
print("小华" not in students) # True
一句话解释:in 在集合里比在列表里快得多,因为它用的是哈希查找。
集合的四种运算——交并差对称差
这是集合的精华部分!用生活例子讲:
- 交集(&):同时出现在两个集合的元素。A 班和 B 班都选了的课
- 并集(|):两个集合合并。A 班或 B 班任何一人选了的课
- 差集(-):只在第一个集合里有的。A 班独有的课
- 对称差集(^):两个集合互相没有的元素。A 班和 B 班互相独有的课
# A 班同学
class_a = {"小明", "小红", "小刚", "小美"}
# B 班同学
class_b = {"小红", "小刚", "小丽", "小强"}
# 交集:两个班都有的同学
both = class_a & class_b
print(f"两个班都有:{both}") # {'小红', '小刚'}
# 并集:至少在一个班出现的同学
all_students = class_a | class_b
print(f"所有同学:{all_students}") # {'小明', '小红', '小刚', '小美', '小丽', '小强'}
# 差集:A 班独有的同学
only_a = class_a - class_b
print(f"A班独有:{only_a}") # {'小明', '小美'}
# 对称差集:只在一个班出现的同学
only_one = class_a ^ class_b
print(f"只在一个班:{only_one}") # {'小明', '小美', '小丽', '小强'}
一句话解释:交集是"共同爱好",并集是"朋友圈合并",差集是"A 的私有物品",对称差集是"互相看不顺眼的"。

frozenset——不可变的集合
有时候你不希望集合被修改,比如放在字典里当 key,或者需要线程安全。
frozenset 就是"冻住"的集合,不能 add、不能 remove、不能 update:
# 普通集合可变
mutable = {1, 2, 3}
mutable.add(4)
print(mutable) # {1, 2, 3, 4}
# frozenset 不可变
frozen = frozenset([1, 2, 3])
# frozen.add(4) # 报错!AttributeError
# frozenset 可以当字典的 key
grade_map = {
frozenset(["数学", "物理"]): "理科学霸",
frozenset(["语文", "英语"]): "文科学霸"
}
print(grade_map) # {frozenset({'数学', '物理'}): '理科学霸', ...}
一句话解释:frozenset 就像装在玻璃框里的水果标本,只能看,不能动。
🔥 实战 35 分钟:3 个递进的小项目
项目 1:学生成绩去重(5 分钟)
需求:一份成绩单里有重复的学号,需要统计有多少不重复的学生。
# 成绩记录(学号, 姓名, 分数)
grade_records = [
("2024001", "小明", 85),
("2024002", "小红", 92),
("2024001", "小明", 85), # 重复!
("2024003", "小刚", 78),
("2024002", "小红", 92), # 重复!
("2024004", "小美", 88),
]
# 用集合提取不重复的学号
unique_ids = set()
for record in grade_records:
student_id = record[0]
unique_ids.add(student_id)
print(f"不重复学生数:{len(unique_ids)}")
print(f"学号列表:{sorted(unique_ids)}")
# 预期输出:
# 不重复学生数:4
# 学号列表:['2024001', '2024002', '2024003', '2024004']
一句话解释:每次遍历成绩单,就把学号扔进集合里,重复的自动被过滤掉了。
项目 2:电商数据分析——找出活跃用户(15 分钟)
需求:有两个月的用户访问记录 CSV,找出:
1. 两个月都活跃的用户(交集)
2. 至少一个月活跃的用户(并集)
3. 只在上个月活跃的用户(差集)
# 模拟两个月的访问日志(实际项目中从 CSV 读取)
march_log = ["user_101", "user_102", "user_103", "user_104", "user_105",
"user_101", "user_102", "user_103"] # user_101 访问了2次
april_log = ["user_102", "user_103", "user_106", "user_107", "user_102"]
# 转成集合去重
march_users = set(march_log)
april_users = set(april_log)
print(f"3月活跃用户:{march_users}")
print(f"4月活跃用户:{april_users}")
# 交集:两个月都活跃
active_both = march_users & april_users
print(f"\n两个月都活跃:{active_both}")
print(f"人数:{len(active_both)}")
# 并集:至少一个月活跃
active_any = march_users | april_users
print(f"\n至少一个月活跃:{active_any}")
print(f"总人数:{len(active_any)}")
# 差集:只在上个月活跃
only_march = march_users - april_users
print(f"\n只在上月活跃:{only_march}")
# 预期输出:
# 3月活跃用户:{'user_101', 'user_102', 'user_103', 'user_104', 'user_105'}
# 4月活跃用户:{'user_102', 'user_103', 'user_106', 'user_107'}
#
# 两个月都活跃:{'user_102', 'user_103'}
# 人数:2
#
# 至少一个月活跃:{'user_101', 'user_102', 'user_103', 'user_104', 'user_105', 'user_106', 'user_107'}
# 总人数:7
#
# 只在上月活跃:{'user_101', 'user_104', 'user_105'}
一句话解释:集合运算让你用 1 行代码就完成了过去要几十行循环才能搞定的事情。
项目 3:新闻关键词提取工具(15 分钟)
需求:做一个简单的关键词提取工具:
1. 从多篇文章中提取所有不重复的关键词
2. 找出两篇文章的共同关键词
3. 找出某篇文章独有的关键词
# 模拟新闻文章(实际项目中从 API/文件读取)
article_1 = """
Python 编程语言越来越受欢迎,越来越多的开发者开始学习 Python。
Python 的简洁语法让编程变得更加容易,Python 适合初学者入门。
"""
article_2 = """
JavaScript 是前端开发的主流语言,Python 也可以用于 Web 开发。
Python 在数据科学领域表现突出,越来越多的人开始使用 Python。
"""
# 简单的中文分词(实际项目用 jieba 库)
def simple_tokenize(text):
"""模拟分词:把文本按空格和标点符号分割"""
# 去除标点符号
for punc in ",。、!…—":
text = text.replace(punc, " ")
# 分割并过滤空字符串
words = [w.strip() for w in text.split() if w.strip()]
return words
# 提取两篇文章的关键词
keywords_1 = set(simple_tokenize(article_1))
keywords_2 = set(simple_tokenize(article_2))
print(f"文章1关键词:{keywords_1}")
print(f"文章2关键词:{keywords_2}")
# 找出共同关键词
common = keywords_1 & keywords_2
print(f"\n共同关键词:{common}")
# 找出文章1独有的关键词
unique_1 = keywords_1 - keywords_2
print(f"文章1独有:{unique_1}")
# 找出文章2独有的关键词
unique_2 = keywords_2 - keywords_1
print(f"文章2独有:{unique_2}")
# 所有关键词(去重后)
all_keywords = keywords_1 | keywords_2
print(f"\n全部关键词数:{len(all_keywords)}")
print(f"去重后关键词:{sorted(all_keywords)}")
# 预期输出:
# 文章1关键词:{'Python', '编程', '语言', '越来越', '受欢迎', '的', ...}
# 文章2关键词:{'JavaScript', '前端', '开发', '主流', '语言', 'Python', ...}
#
# 共同关键词:{'Python', '语言', '越来越', '的', ...}
# 文章1独有:{'编程', '简洁', '语法', '让', '变得', '容易', '适合', '初学者', '入门'}
# 文章2独有:{'JavaScript', '前端', '开发', '主流', '在', '数据', '科学', '领域', '表现', '突出', '越来越', '多', '的', '人', '开始', '使用'}
# 统计关键词出现频率
keyword_count = {}
for keyword in all_keywords:
count_1 = keywords_1.count(keyword) if keyword in keywords_1 else 0
count_2 = keywords_2.count(keyword) if keyword in keywords_2 else 0
keyword_count[keyword] = {"文章1": count_1, "文章2": count_2}
print("\n关键词统计:")
for kw, counts in sorted(keyword_count.items()):
print(f" {kw}: 文章1={counts['文章1']}次, 文章2={counts['文章2']}次")
一句话解释:用集合的去重能力处理文本,先分词再转集合,两篇文章的关键词对比一目了然。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:空集合不能用 {}
# ❌ 错误写法
empty = {} # 这是字典,不是集合!
print(type(empty)) # <class 'dict'>
# ✅ 正确写法
empty = set()
print(type(empty)) # <class 'set'>
一句话解释:{} 在 Python 里是字典的专属符号,想要空集合必须用 set()。
坑 2:集合里的元素必须是可哈希的
集合元素必须是不可变类型(能 hash 的),列表和字典不能放进去:
# ❌ 错误:列表是可变的,不能放集合里
bad_set = {{1, 2}, {3, 4}} # 报错!TypeError
# ❌ 错误:字典也是可变的
bad_set = {{"a": 1}} # 报错!
# ✅ 正确:元组是不可变的,可以放
good_set = {(1, 2), (3, 4)}
print(good_set) # {(1, 2), (3, 4)}
# ✅ 正确:字符串、数字都可以
mixed = {"hello", 123, (1, 2, 3)}
print(mixed) # {'hello', 123, (1, 2, 3)}
一句话解释:集合是"水果篮子",只能放"完整的水果"(不可变对象),不能放"水果拼盘"(可变对象)。
坑 3:集合的 + 不是合并
# ❌ 错误:集合不支持 + 运算符
a = {1, 2, 3}
b = {4, 5, 6}
# result = a + b # 报错!TypeError
# ✅ 正确:用 | 或 update
result = a | b # 返回新集合
print(result) # {1, 2, 3, 4, 5, 6}
# 或者原地合并
a.update(b)
print(a) # {1, 2, 3, 4, 5, 6}
一句话解释:数学里用 + 表示合并,但 Python 集合里要用 | 管道符。
坑 4:集合的 in 找不到会报错?
不会!集合的 in 操作符永远返回 True/False,不会抛异常:
s = {1, 2, 3}
print(100 in s) # False,不会报错
一句话解释:集合的成员判断是"安全的",要么找到返回 True,要么找不到返回 False。
坑 5:集合元素顺序不可依赖
# ❌ 错误:依赖集合的顺序
s = {3, 1, 2}
print(s) # 可能输出 {1, 2, 3},也可能不是!
# ✅ 正确:如果需要顺序,转成列表
s = {3, 1, 2}
ordered = sorted(s) # [1, 2, 3]
print(ordered)
一句话解释:集合是"水果篮子",水果在里面乱放,如果要按顺序拿,得先排好队(sorted)。
性能小贴士:批量添加用 update
# ❌ 慢:逐个添加
s = set()
for item in range(10000):
s.add(item)
# ✅ 快:批量添加
s = set(range(10000)) # 一步到位
一句话解释:一次性往篮子里放 100 个水果,比一个一个放快得多。
调试技巧:用 print 检查集合状态
def find_common_users(active_users, blocked_users):
"""找活跃且未拉黑的用户"""
print(f"活跃用户:{active_users}")
print(f"拉黑用户:{blocked_users}")
# 先看差集
not_blocked = active_users - blocked_users
print(f"活跃且未拉黑:{not_blocked}")
return not_blocked
users = {"小明", "小红", "小刚"}
blocked = {"小刚"}
result = find_common_users(users, blocked)
# 输出:
# 活跃用户:{'小明', '小红', '小刚'}
# 拉黑用户:{'小刚'}
# 活跃且未拉黑:{'小明', '小红'}
一句话解释:在关键步骤打印集合,能快速定位问题在哪一步。
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(1 分钟):基础——添加元素
- 输入:s = {1, 2, 3},添加数字 4
- 预期输出:{1, 2, 3, 4}
- 提示:add() 方法
练习 2(2 分钟):基础——去重判断
- 输入:["苹果", "香蕉", "苹果", "橙子", "香蕉", "苹果"]
- 预期输出:{'苹果', '香蕉', '橙子'}
- 提示:转成 set 再转回 list
练习 3(2 分钟):进阶——集合运算
- 输入:A = {1, 2, 3, 4},B = {3, 4, 5, 6},求交集
- 预期输出:{3, 4}
- 提示:用 & 运算符
练习 4(3 分钟):进阶——用户名单对比
- 输入:old_users = {"小明", "小红"},new_users = {"小红", "小刚"}
- 问题:哪些是新来的?哪些走了?
- 预期输出:新来:{'小刚'},离开:{'小明'}
- 提示:用差集 new - old 是新来的,old - new 是离开的
练习 5(2 分钟):挑战——找错
- 以下代码报错,找出问题并修复:
my_set = {1, 2, [3, 4]} # 报错!
my_set.add(5)
print(my_set)
- 预期输出:
{1, 2, [3, 4], 5}或类似结果 - 提示:列表不能放集合里,改成元组
作业题(30 分钟 - 2 小时)
作业:做一个「重复代码检测器」
需求描述:
你是一个代码审查员,写了一个检查文件夹里有没有重复代码的小工具。
功能点:
1. 读取多个 Python 文件(模拟几个文件内容)
2. 提取每个文件的所有代码行(去空格空行)
3. 找出哪些行在多个文件里出现过
4. 输出每个重复行的"出现在哪些文件"
加分项:
1. 统计每个文件有多少行代码,多少行是重复的
2. 找出完全一模一样的文件(所有行都相同)
验收标准:
- 能跑起来
- 输出结果符合预期
- 代码有注释(每段干嘛的写清楚)
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章核心 3 点
- 集合是天然去重的"水果篮子":用
set()创建,add()添加,in判断成员 - 四种运算解决"有没有"的问题:交
&、并|、差-、对称差^ - frozenset 不可变,可当字典 key:需要哈希的安全数据就用它
延伸学习资源
- Python 官方文档 - set:权威文档,例子丰富
- 《Python编程:从入门到实践》第 4 章:项目驱动,适合动手党
- 视频:B 站「小甲鱼」Python 教程第 38 讲——集合部分讲得很生动
互动钩子
你在处理用户数据时遇到过重复记录吗?是怎么处理的?评论区聊聊你是用列表还是集合,老粉优先回复!
下一章我们要做一个通讯录管理系统,会综合用到字典和集合:字典存储联系人详情,集合处理标签分类。敬请期待!

评论(0)