P2F-Python集合完全指南-从创建到去重集合运算的Python编程利器
📝 摘要
集合(Set)是 Python 中一种非常实用的数据结构,核心特点是无序且唯一。它最大的用途就是去重,另外还有高效的成员检测和丰富的集合运算(并集、交集、差集等)。本文我们将从零开始,系统学习集合的创建、基本操作、运算方法、去重应用,以及不可变的 frozenset。通过大量代码示例和图表,帮助我们快速掌握集合的各种用法 💪
1. 前置知识点 📚
在开始学习集合之前,我们需要先了解几个基础概念 📖
可迭代对象:可以逐个访问元素的对象,如列表、元组、字符串等。
可变 vs 不可变:
- 可变:创建后可修改(列表、字典、集合)
- 不可变:创建后不能修改(数字、字符串、元组)
哈希(Hash):快速查找技术。不可变对象有固定哈希值,可变对象不能哈希。
如果这些概念不太懂,可以看看:
2. 什么是集合(Set)📖
2.1 集合的核心特点 🔍
集合这货啊,简单来说就是"无序且不重复"的元素容器 🔩。它有四个核心特点,我们来一个个看:
1. 无序性 🎲
集合里的元素没有固定顺序,不支持下标访问。也就是说,我们不能用 set[0] 这种方式来取元素。那怎么访问呢?只能通过遍历或者直接判断元素是否存在。好处是什么呢?就是我们不需要关心元素的排列顺序,只要关心"这个元素在不在集合里"就行。
s = {'a', 'b', 'c', 'd', 'e', 'f'}
print(s) # 每次输出顺序可能不同,例如: {'c', 'a', 'f', 'd', 'b', 'e'}
2. 唯一性 ✨
这是集合最迷人的特点——自动去重!当我们往集合里添加重复元素时,Python 会自动忽略它。这个特性在实际开发中超级实用,比如我们想找出列表中不重复的元素,直接转成集合就搞定了。
s = {1, 2, 3, 2, 1, 4, 3}
print(s) # 输出: {1, 2, 3, 4} - 自动去重了!
3. 可变性 🔧
集合本身是可以修改的,我们可以随时添加或删除元素。不过要注意,集合里的元素必须是不可变类型,比如字符串、数字、元组等。列表和字典这种可变类型是不能作为集合元素的,因为我们没办法确定它的哈希值。
那什么是不可变类型呢?简单来说,就是"创建后不能改变"的数据类型。在 Python 中:
- 可哈希(hashable)的类型:int、float、str、bool、None、tuple(且元组内全是不可变元素)、frozenset
- 不可哈希(unhashable)的类型:list、dict、set
为什么集合元素必须可哈希呢?因为集合底层用到了哈希表技术,它需要通过元素的哈希值来快速定位元素。如果元素是可以改变的,那它的哈希值就可能变化,集合就乱了套了。所以 Python 直接规定:集合元素必须是不可变类型!
# 这些可以:
s1 = {1, 2, 3} # 整数
s2 = {'a', 'b', 'c'} # 字符串
s3 = {(1, 2), (3, 4)} # 元组(元素都是不可变的)
# 这些会报错:
s4 = {[1, 2], [3, 4]} # ❌ TypeError: unhashable type: 'list'
s5 = {{'a': 1}} # ❌ TypeError: unhashable type: 'dict'
s6 = {{1, 2}} # ❌ TypeError: unhashable type: 'set'
4. 高效操作 ⚡
集合底层基于哈希表实现,这意味着查找、添加、删除元素的时间复杂度都是 O(1),也就是常数时间。无论集合里有100个元素还是100万个元素,操作速度都差不多。这可比列表的线性查找快多了!
2.2 集合 vs 列表 vs 元组对比 ⚖️
光说集合可能不太好理解,我们来把它和列表、元组放一起对比看看 📊:
| 特性 | 集合(set) | 列表(list) | 元组(tuple) |
|---|---|---|---|
| 有序性 | ❌ 无序 | ✅ 有序 | ✅ 有序 |
| 唯一性 | ✅ 自动去重 | ❌ 可以重复 | ❌ 可以重复 |
| 可变性 | ✅ 可变 | ✅ 可变 | ❌ 不可变 |
| 索引访问 | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| 元素类型 | 必须是不可变类型 | 可以是任意类型 | 可以是任意类型 |
| 查找效率 | O(1) 极快 | O(n) 较慢 | O(n) 较慢 |
| 内存占用 | 较高 | 较低 | 最低 |
从表格能看出来,集合最适合的场景就是:需要快速查找、需要去重、需要集合运算的时候 🏆。如果你需要保持元素顺序或者通过索引访问,那就用列表;如果你需要存储固定数据、追求极致性能,那就用元组。
3. 集合创建(set creation)✍️
3.1 使用花括号创建 🅰️
这是最直接的方式,直接用花括号把元素括起来,元素之间用逗号分隔:
# 基本创建
fruits = {"apple", "banana", "orange"}
print(fruits) # {'banana', 'orange', 'apple'}
# 数字集合
numbers = {1, 2, 3, 4, 5}
print(numbers) # {1, 2, 3, 4, 5}
# 混合类型(只要是不可变类型就行)
mixed = {"hello", 123, (1, 2, 3)}
print(mixed) # {'hello', 123, (1, 2, 3)}
还有一点很方便——自动去重!如果我们写重复的元素,Python 会自动帮我们去掉:
s = {1, 2, 3, 2, 1, 4, 3}
print(s) # {1, 2, 3, 4} - 自动去重了!
不过这里有个坑:空的花括号 {} 创建的不是空集合,而是空字典! 要创建空集合必须用 set()(详见 3.2 使用 set() 函数创建)。
# ❌ 这是字典,不是集合!
empty_dict = {}
print(type(empty_dict)) # <class 'dict'>
# ✅ 这才是空集合
empty_set = set()
print(type(empty_set)) # <class 'set'>
print(empty_set) # set()
3.2 使用 set() 函数创建 🅱️
set() 函数可以创建集合,还有几个常用用法:
1. 创建空集合
empty_set = set()
print(empty_set) # set()
2. 从列表、元组、字符串转换
这个功能超级实用!我们可以把其他可迭代对象转成集合,自动去重:
# 从列表创建(自动去重)
lst = [1, 2, 3, 2, 1, 4]
s1 = set(lst)
print(s1) # {1, 2, 3, 4}
# 从元组创建
tup = (1, 2, 3, 2, 1)
s2 = set(tup)
print(s2) # {1, 2, 3}
# 从字符串创建(把字符拆成集合,自动去重)
text = "hello"
s3 = set(text)
print(s3) # {'h', 'e', 'l', 'o'} - 注意 'l' 只出现一次
3. 从集合创建(复制)
original = {1, 2, 3}
copied = set(original)
print(copied) # {1, 2, 3}
3.3 集合推导式创建 🦾
和列表推导式类似,集合也有推导式!语法几乎一样,只是把方括号换成花括号。如果你想了解更多推导式的细节,可以看看 P5D-Python_推导式完全指南 📚:
# 基本语法
squares = {x**2 for x in range(1, 6)}
print(squares) # {16, 1, 4, 9, 25}
# 带条件筛选
even_squares = {x**2 for x in range(1, 10) if x % 2 == 0}
print(even_squares) # {16, 4, 36, 64}
# 从字符串过滤
text = "hello world"
unique_vowels = {char for char in text if char in 'aeiou'}
print(unique_vowels) # {'e', 'o'}
# 复杂一点的条件
pairs = {(x, y) for x in range(3) for y in range(3)}
print(pairs) # {(0, 0), (0, 1), (0, 2), (1, 0), ...}
集合推导式和列表推导式的区别:
- 列表推导式用方括号
[] - 集合推导式用花括号
{} - 集合会自动去重,列表不会
3.4 集合创建方式对比 📊
我们来对比一下这三种创建方式:
| 创建方式 | 适用场景 | 示例 |
|---|---|---|
花括号 {} | 已知具体元素,直观简洁 | {1, 2, 3} |
set() 函数 | 从其他数据转换、创建空集合 | set([1,2,3])、set() |
| 集合推导式 | 批量生成、有筛选逻辑 | {x**2 for x in range(5)} |
使用建议:
- 如果知道具体元素,用花括号最直观
- 如果要从列表去重,用
set() - 如果要批量生成有规律的元素,用推导式
4. 集合基本操作 🛠️
4.1 访问集合中的元素
我们前面说过,集合是无序的,所以不能用索引来访问元素。那怎么访问呢?两个办法:遍历和成员检测 🔍
1. 遍历集合
fruits = {"apple", "banana", "orange"}
# 用 for 循环遍历
for fruit in fruits:
print(fruit) # 每次顺序可能不同
2. 成员检测
想知道某个元素在不在集合里?用 in 操作符,超快!
fruits = {"apple", "banana", "orange"}
print("apple" in fruits) # True
print("grape" in fruits) # False
这就是集合最强大的地方——成员检测超级快!无论集合里有10个还是100万个元素,in 操作都是 O(1) 复杂度。
4.2 添加元素 ➕
往集合里添加元素有两种方式:
1. add() - 添加单个元素
s = {1, 2, 3}
s.add(4)
print(s) # {1, 2, 3, 4}
# 如果添加已存在的元素,什么都不会发生(自动去重)
s.add(2)
print(s) # {1, 2, 3, 4} - 2 已经在集合里了
2. update() - 添加多个元素
s = {1, 2, 3}
s.update([4, 5, 6]) # 从列表添加
print(s) # {1, 2, 3, 4, 5, 6}
s.update((7, 8)) # 从元组添加
print(s) # {1, 2, 3, 4, 5, 6, 7, 8}
s.update({9, 10}) # 从集合添加
print(s) # {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s.update("hello") # 从字符串添加(每个字符)
print(s) # {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'h', 'e', 'l', 'o'}
4.3 删除元素 🗑️
删除元素有四种方式,各有特点:
1. remove() - 删除指定元素(元素不存在会报错)
s = {1, 2, 3, 4, 5}
s.remove(3)
print(s) # {1, 2, 4, 5}
# 如果元素不存在,会抛出 KeyError
s.remove(100) # KeyError: 100
2. discard() - 删除指定元素(元素不存在不会报错)
s = {1, 2, 3, 4, 5}
s.discard(3)
print(s) # {1, 2, 4, 5}
# 元素不存在也不会报错,安全感满满!
s.discard(100) # 什么都不发生,程序继续运行
3. pop() - 随机删除并返回一个元素
s = {1, 2, 3, 4, 5}
removed = s.pop()
print(f"删除的元素: {removed}") # 随机一个元素
print(f"剩余集合: {s}")
# 空集合调用 pop() 会报错
empty_set = set()
empty_set.pop() # KeyError: 'pop from an empty set'
4. clear() - 清空集合
s = {1, 2, 3, 4, 5}
s.clear()
print(s) # set() - 变成空集合了
4.4 计算集合长度
想知道集合里有多少个元素?用 len() 函数:
s = {1, 2, 3, 4, 5}
print(len(s)) # 5
# 自动去重,所以长度会变
s = {1, 1, 1, 2, 2, 3}
print(len(s)) # 3
我们来看个对比表格 🧮:
| 方法 | 作用 | 特点 |
|---|---|---|
add(x) | 添加单个元素 | 元素已存在则忽略 |
update(iterable) | 添加多个元素 | 可接受列表、元组、集合、字符串 |
remove(x) | 删除指定元素 | 元素不存在会报错 |
discard(x) | 删除指定元素 | 元素不存在不报错 |
pop() | 随机删除一个 | 返回被删除的元素,空集合报错 |
clear() | 清空集合 | 集合变为空集合 |
5. 集合运算 ⚔️
5.1 并集(Union)🔗
并集就是把两个集合的所有元素合并在一起,重复的只保留一个。想象一下两个班的学生,合并成一个班 🎉
运算符方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 | 运算符
result = set1 | set2
print(result) # {1, 2, 3, 4, 5}
方法方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 union() 方法
result = set1.union(set2)
print(result) # {1, 2, 3, 4, 5}
# union() 可以接收多个参数
result = set1.union(set2, {6, 7})
print(result) # {1, 2, 3, 4, 5, 6, 7}
我们来看个图示 🎨:
flowchart TB
subgraph A[集合A]
direction LR
A1["1"]
A2["2"]
A3["3"]
end
subgraph B[集合B]
direction LR
B1["3"]
B2["4"]
B3["5"]
end
A & B --> |并集| R[结果: 1,2,3,4,5]
style R fill:#90EE90
5.2 交集(Intersection)🎯
交集就是两个集合中都有的元素。想象一下两个班都参加了某项活动的学生 👥
运算符方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 & 运算符
result = set1 & set2
print(result) # {3}
方法方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 intersection() 方法
result = set1.intersection(set2)
print(result) # {3}
# intersection() 可以接收多个参数
set3 = {3, 5, 6}
result = set1.intersection(set2, set3)
print(result) # {3}
图示 🎨:
flowchart TB
subgraph A[集合A]
direction LR
A1["1"]
A2["2"]
A3["3"]
end
subgraph B[集合B]
direction LR
B1["3"]
B2["4"]
B3["5"]
end
A & B --> |交集| R[结果: 3]
style R fill:#90EE90
5.3 差集(Difference)➖
差集就是 A 集合中有但 B 集合中没有的元素。想象一下参加了A活动但没参加B活动的学生 📋
运算符方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 - 运算符
result = set1 - set2
print(result) # {1, 2}
# 注意顺序:set2 - set1 结果不同
result = set2 - set1
print(result) # {4, 5}
方法方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 difference() 方法
result = set1.difference(set2)
print(result) # {1, 2}
图示 🎨:
flowchart TB
subgraph A[集合A: 1,2,3]
direction LR
A1["1"]
A2["2"]
A3["3"]
end
subgraph B[集合B: 3,4,5]
direction LR
B1["3"]
B2["4"]
B3["5"]
end
A -->|减去| B
B -.->|被移除| B1
A1 & A2 --> |保留| R[结果: 1,2]
style R fill:#90EE90
5.4 对称差集(Symmetric Difference)🔄
对称差集就是 A 和 B 的并集减去交集,也就是只属于其中一个集合的元素。想象一下参加了A或B活动(但不同时参加两个)的学生 🤔
运算符方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 ^ 运算符
result = set1 ^ set2
print(result) # {1, 2, 4, 5}
方法方式
set1 = {1, 2, 3}
set2 = {3, 4, 5}
# 用 symmetric_difference() 方法
result = set1.symmetric_difference(set2)
print(result) # {1, 2, 4, 5}
图示 🎨:
flowchart TB
subgraph A[集合A]
direction LR
A1["1"]
A2["2"]
A3["3"]
end
subgraph B[集合B]
direction LR
B1["3"]
B2["4"]
B3["5"]
end
A & B --> |对称差集| R[结果: 1,2,4,5]
style R fill:#90EE90
5.5 子集和超集 📊
子集和超集用来判断集合之间的关系:
- 子集:A 是 B 的子集,说明 A 的所有元素 B 都有
- 超集:A 是 B 的超集,说明 A 包含了 B 的所有元素
子集判断
A = {1, 2, 3}
B = {1, 2, 3, 4, 5}
# 用 <= 判断 A 是否是 B 的子集
print(A <= B) # True
print(A.issubset(B)) # True
# 用 < 判断 A 是否是 B 的真子集(不是相等的情况)
print(A < B) # True
print(A == B) # False
超集判断
A = {1, 2, 3, 4, 5}
B = {1, 2, 3}
# 用 >= 判断 A 是否是 B 的超集
print(A >= B) # True
print(A.issuperset(B)) # True
# 用 > 判断 A 是否是 B 的真超集
print(A > B) # True
print(A == B) # False
我们来看个总结表格 📊:
| 运算 | 运算符 | 方法 | 说明 |
|---|---|---|---|
| 并集 | | | union() | 所有元素合并 |
| 交集 | & | intersection() | 共同元素 |
| 差集 | - | difference() | A有B没有 |
| 对称差集 | ^ | symmetric_difference() | 仅属于其中一个 |
| 子集 | <= | issubset() | A全在B里 |
| 真子集 | < | - | A全在B里且不相等 |
| 超集 | >= | issuperset() | B全在A里 |
| 真超集 | > | - | B全在A里且不相等 |
6. 集合方法 🔧
我们在前面已经学过增删方法(add、remove、discard、pop、clear)和运算方法(union、intersection、difference)。现在我们来聊聊那些原地修改的运算方法,也就是带 _update 后缀的这些方法 🔧
6.1 增删方法
这部分内容我们在 第四章 已经详细介绍过了,这里简单回顾一下:
| 方法 | 作用 |
|---|---|
add(x) | 添加单个元素 |
update(iterable) | 添加多个元素 |
remove(x) | 删除指定元素,不存在会报错 |
discard(x) | 删除指定元素,不存在不报错 |
pop() | 随机删除并返回元素 |
clear() | 清空集合 |
copy() | 复制集合 |
6.2 原地运算方法
这些方法会直接修改原集合,而不是返回一个新集合。它们的名字都有 _update 后缀 ⚡
1. intersection_update() - 原地求交集
s1 = {1, 2, 3, 4}
s2 = {3, 4, 5, 6}
# 求交集,并原地修改 s1
s1.intersection_update(s2)
print(s1) # {3, 4}
2. difference_update() - 原地求差集
s1 = {1, 2, 3, 4}
s2 = {3, 4, 5, 6}
# 求差集,并原地修改 s1
s1.difference_update(s2)
print(s1) # {1, 2}
3. symmetric_difference_update() - 原地求对称差集
s1 = {1, 2, 3, 4}
s2 = {3, 4, 5, 6}
# 求对称差集,并原地修改 s1
s1.symmetric_difference_update(s2)
print(s1) # {1, 2, 5, 6}
4. update() - 原地求并集
s1 = {1, 2, 3}
s2 = {3, 4, 5}
# 求并集,并原地修改 s1
s1.update(s2)
print(s1) # {1, 2, 3, 4, 5}
我们来看个对比表格 📊:
| 方法 | 作用 | 原集合变化 |
|---|---|---|
union() | 返回并集 | 不变 |
update() | 原地并集 | 修改原集合 |
intersection() | 返回交集 | 不变 |
intersection_update() | 原地交集 | 修改原集合 |
difference() | 返回差集 | 不变 |
difference_update() | 原地差集 | 修改原集合 |
symmetric_difference() | 返回对称差集 | 不变 |
symmetric_difference_update() | 原地对称差集 | 修改原集合 |
使用场景:如果我们不需要保留原集合,用原地运算方法可以节省内存;如果需要保留原集合,用返回新集合的方法。
7. 集合去重应用 💡
集合最大的用处就是去重,我们来看看几种常见场景 🧹
7.1 列表去重
lst = [1, 2, 3, 2, 1, 4, 3]
unique = list(set(lst))
print(unique) # [1, 2, 3, 4] - 无序
# 如果需要保持顺序,用 dict.fromkeys()
# 原理:Python 3.7+ 字典保持插入顺序,我们利用键的唯一性来去重
unique_ordered = list(dict.fromkeys(lst))
print(unique_ordered) # [1, 2, 3, 4]
7.2 字符串去重
text = "hello world"
unique_chars = set(text)
print(unique_chars) # {'h', 'e', 'l', 'o', ' ', 'w', 'r', 'd'}
# 按原顺序 - 利用字典键的唯一性和保持顺序的特性
unique_ordered = ''.join(dict.fromkeys(text))
print(unique_ordered) # helo wrd
7.3 复杂数据去重
对于列表里的复杂数据(字典、元组等),可以用 dict.fromkeys():
data = [{'a': 1}, {'b': 2}, {'a': 1}, {'b': 2}]
unique = list(dict.fromkeys(data))
print(unique) # [{'a': 1}, {'b': 2}]
8. frozenset:不可变集合 🔐
frozenset 就是"冻住"的集合——不可变的集合。它是 set 的兄弟,但一旦创建就不能修改 🔒
8.1 frozenset 的创建
# 从可迭代对象创建
fs1 = frozenset([1, 2, 3, 4, 5])
print(fs1) # frozenset({1, 2, 3, 4, 5})
# 从字符串创建
fs2 = frozenset("hello")
print(fs2) # frozenset({'h', 'e', 'l', 'o'})
# 从集合创建
fs3 = frozenset({1, 2, 3})
print(fs3) # frozenset({1, 2, 3})
8.2 frozenset 的使用场景
1. 作为字典的键
因为 frozenset 是不可变的,所以它可以哈希,能作为字典的键:
# 用 frozenset 作为字典的键
favorites = {
frozenset(['apple', 'banana']): '水果',
frozenset(['chicken', 'beef']): '肉类'
}
print(favorites) # {frozenset({'apple', 'banana'}): '水果', ...}
2. 作为集合的元素
普通的 set 是不能作为另一个 set 的元素的(因为 set 是可变的),但 frozenset 可以:
# 集合的集合 - 普通 set 不行
set_of_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(set_of_sets) # {frozenset({1, 2}), frozenset({3, 4})}
3. 需要哈希的时候
frozenset 是可哈希的,普通 set 不行:
fs = frozenset([1, 2, 3])
print(hash(fs)) # 可以哈希
s = {1, 2, 3}
# print(hash(s)) # TypeError: unhashable type: 'set'
我们来看个对比表格 📊:
| 特性 | set | frozenset |
|---|---|---|
| 可变性 | 可变 | 不可变 |
| 作为字典键 | ❌ 不行 | ✅ 可以 |
| 作为集合元素 | ❌ 不行 | ✅ 可以 |
| 哈希 | ❌ 不可哈希 | ✅ 可哈希 |
9. 总结 🎉
到这里,我们就把 Python 集合的内容都学完了!我们来回顾一下 📚
集合的核心特点:
- 无序:不能用索引访问
- 唯一:自动去重
- 高效:查找、添加、删除都是 O(1)
常用的操作:
- 创建:
{}、set()、集合推导式 - 增删:add、remove、discard、pop、clear
- 运算:并集、交集、差集、对称差集
- 判断:子集、超集
特殊类型:
- frozenset:不可变集合,可以作为字典键或集合元素
集合是 Python 中非常强大的数据结构,特别适合需要去重、快速查找、集合运算的场景。多练习多用,你会发现它的魅力的!💪
最后更新时间:2026-04-10