解压整理收纳师
68.55M · 2026-04-12
集合(Set)是 Python 中一种非常实用的数据结构,核心特点是无序且唯一。它最大的用途就是去重,另外还有高效的成员检测和丰富的集合运算(并集、交集、差集等)。本文我们将从零开始,系统学习集合的创建、基本操作、运算方法、去重应用,以及不可变的 frozenset。通过大量代码示例和图表,帮助我们快速掌握集合的各种用法
在开始学习集合之前,我们需要先了解几个基础概念
可迭代对象:可以逐个访问元素的对象,如列表、元组、字符串等。
可变 vs 不可变:
哈希(Hash):快速查找技术。不可变对象有固定哈希值,可变对象不能哈希。
如果这些概念不太懂,可以看看:
集合这货啊,简单来说就是"无序且不重复"的元素容器 。它有四个核心特点,我们来一个个看:
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 中:
为什么集合元素必须可哈希呢?因为集合底层用到了哈希表技术,它需要通过元素的哈希值来快速定位元素。如果元素是可以改变的,那它的哈希值就可能变化,集合就乱了套了。所以 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万个元素,操作速度都差不多。这可比列表的线性查找快多了!
光说集合可能不太好理解,我们来把它和列表、元组放一起对比看看 :
| 特性 | 集合(set) | 列表(list) | 元组(tuple) |
|---|---|---|---|
| 有序性 | 无序 | 有序 | 有序 |
| 唯一性 | 自动去重 | 可以重复 | 可以重复 |
| 可变性 | 可变 | 可变 | 不可变 |
| 索引访问 | 不支持 | 支持 | 支持 |
| 元素类型 | 必须是不可变类型 | 可以是任意类型 | 可以是任意类型 |
| 查找效率 | O(1) 极快 | O(n) 较慢 | O(n) 较慢 |
| 内存占用 | 较高 | 较低 | 最低 |
从表格能看出来,集合最适合的场景就是:需要快速查找、需要去重、需要集合运算的时候 。如果你需要保持元素顺序或者通过索引访问,那就用列表;如果你需要存储固定数据、追求极致性能,那就用元组。
这是最直接的方式,直接用花括号把元素括起来,元素之间用逗号分隔:
# 基本创建
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()
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}
和列表推导式类似,集合也有推导式!语法几乎一样,只是把方括号换成花括号。如果你想了解更多推导式的细节,可以看看 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), ...}
集合推导式和列表推导式的区别:
[]{}我们来对比一下这三种创建方式:
| 创建方式 | 适用场景 | 示例 |
|---|---|---|
花括号 {} | 已知具体元素,直观简洁 | {1, 2, 3} |
set() 函数 | 从其他数据转换、创建空集合 | set([1,2,3])、set() |
| 集合推导式 | 批量生成、有筛选逻辑 | {x**2 for x in range(5)} |
使用建议:
set()我们前面说过,集合是无序的,所以不能用索引来访问元素。那怎么访问呢?两个办法:遍历和成员检测
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) 复杂度。
往集合里添加元素有两种方式:
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'}
删除元素有四种方式,各有特点:
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() - 变成空集合了
想知道集合里有多少个元素?用 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() | 清空集合 | 集合变为空集合 |
并集就是把两个集合的所有元素合并在一起,重复的只保留一个。想象一下两个班的学生,合并成一个班
运算符方式
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
交集就是两个集合中都有的元素。想象一下两个班都参加了某项活动的学生
运算符方式
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
差集就是 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
对称差集就是 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
子集和超集用来判断集合之间的关系:
子集判断
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里且不相等 |
我们在前面已经学过增删方法(add、remove、discard、pop、clear)和运算方法(union、intersection、difference)。现在我们来聊聊那些原地修改的运算方法,也就是带 _update 后缀的这些方法
这部分内容我们在 第四章 已经详细介绍过了,这里简单回顾一下:
| 方法 | 作用 |
|---|---|
add(x) | 添加单个元素 |
update(iterable) | 添加多个元素 |
remove(x) | 删除指定元素,不存在会报错 |
discard(x) | 删除指定元素,不存在不报错 |
pop() | 随机删除并返回元素 |
clear() | 清空集合 |
copy() | 复制集合 |
这些方法会直接修改原集合,而不是返回一个新集合。它们的名字都有 _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() | 原地对称差集 | 修改原集合 |
使用场景:如果我们不需要保留原集合,用原地运算方法可以节省内存;如果需要保留原集合,用返回新集合的方法。
集合最大的用处就是去重,我们来看看几种常见场景
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]
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
对于列表里的复杂数据(字典、元组等),可以用 dict.fromkeys():
data = [{'a': 1}, {'b': 2}, {'a': 1}, {'b': 2}]
unique = list(dict.fromkeys(data))
print(unique) # [{'a': 1}, {'b': 2}]
frozenset 就是"冻住"的集合——不可变的集合。它是 set 的兄弟,但一旦创建就不能修改
# 从可迭代对象创建
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})
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 |
|---|---|---|
| 可变性 | 可变 | 不可变 |
| 作为字典键 | 不行 | 可以 |
| 作为集合元素 | 不行 | 可以 |
| 哈希 | 不可哈希 | 可哈希 |
到这里,我们就把 Python 集合的内容都学完了!我们来回顾一下
集合的核心特点:
常用的操作:
{}、set()、集合推导式特殊类型:
集合是 Python 中非常强大的数据结构,特别适合需要去重、快速查找、集合运算的场景。多练习多用,你会发现它的魅力的!
最后更新时间:2026-04-10