17. 集合:处理唯一且无序的数据
在前面的章节中,我们使用过列表(list)(有序、可变的集合)和字典(dictionary)(键值映射)。现在我们将探索集合(set),这是 Python 的一种集合类型,专门用于存储唯一项,并高效执行数学上的集合运算。
当你需要去除重复项、快速测试成员关系,或执行诸如在多个集合之间找出共同元素等操作时,集合尤其强大。与列表不同,集合是无序的,并且不能包含重复值——尝试把同一个元素添加两次不会产生任何效果。
17.1) 创建集合与基本操作
17.1.1) 使用花括号创建集合
创建集合最常见的方式是使用花括号 {},并用逗号分隔各个值:
# 创建一个编程语言集合
languages = {"Python", "JavaScript", "Java", "C++"}
print(languages) # Output: {'Python', 'JavaScript', 'Java', 'C++'}
print(type(languages)) # Output: <class 'set'>重要:当你打印集合时,元素的顺序可能与输入时的顺序不同。集合是无序集合,这意味着 Python 不会维护任何特定的顺序:
numbers = {5, 2, 8, 1, 9}
print(numbers) # Output might be: {1, 2, 5, 8, 9} or another order输出顺序可能会在不同的 Python 运行次数与版本之间变化。永远不要依赖集合保持特定顺序——如果顺序很重要,请改用列表。
17.1.2) 集合会自动移除重复项
集合最有用的特性之一是它会自动消除重复值。如果你尝试创建一个包含重复项的集合,最终只会保留每个唯一值的一份副本:
# 创建一个包含重复值的集合
student_ids = {101, 102, 103, 102, 101, 104}
print(student_ids) # Output: {101, 102, 103, 104}
# 这个特性使集合非常适合用于去重
grades = [85, 90, 85, 78, 90, 92, 78, 85]
unique_grades = set(grades)
print(unique_grades) # Output: {78, 85, 90, 92}这种自动去重之所以会发生,是因为集合使用数学上的集合模型,其中每个元素只能出现一次。当你添加一个已存在的值时,集合会直接忽略重复项。
17.1.3) 使用 set() 构造器创建集合
你可以使用 set() 构造器从其他可迭代对象创建集合。这对于把列表、元组或字符串转换为集合特别有用:
# 从列表创建集合
colors_list = ["red", "blue", "green", "red", "yellow"]
colors_set = set(colors_list)
print(colors_set) # Output: {'red', 'blue', 'green', 'yellow'}
# 从字符串创建集合(每个字符会成为一个元素)
letters = set("programming")
print(letters) # Output: {'p', 'r', 'o', 'g', 'a', 'm', 'i', 'n'}
# 从元组创建集合
coordinates = set((10, 20, 30, 20, 10))
print(coordinates) # Output: {10, 20, 30}当你从字符串创建集合时,每个唯一字符都会成为一个独立元素。这对于找出文本中所有不同字符很有用:
text = "Mississippi"
unique_chars = set(text.lower())
print(unique_chars) # Output: {'m', 'i', 's', 'p'}
print(f"The word contains {len(unique_chars)} unique letters")
# Output: The word contains 4 unique letters17.1.4) 创建空集合
这里有一个关键的坑点:你不能使用 {} 创建空集合,因为 Python 会把它解释为空字典。相反,你必须使用 set():
# WRONG - This creates an empty dictionary, not a set
empty_dict = {}
print(type(empty_dict)) # Output: <class 'dict'>
# CORRECT - This creates an empty set
empty_set = set()
print(type(empty_set)) # Output: <class 'set'>
print(empty_set) # Output: set()之所以有这种区别,是因为字典在 Python 中比集合更早引入,所以 {} 已经被用于表示空字典了。当你打印空集合时,Python 会将其显示为 set() 以避免混淆。
初学者常见困惑:当你使用变量创建只包含一个元素的集合时,集合包含的是变量的值,而不是变量名:
# 理解使用变量创建集合
x = 5
my_set = {x} # Creates {5}, not {'x'}
print(my_set) # Output: {5}
# 如果你想创建一个包含字符串 'x' 的集合:
my_set = {'x'}
print(my_set) # Output: {'x'}
# 这同样适用于任何表达式
result = 10 + 5
my_set = {result} # Creates {15}
print(my_set) # Output: {15}17.1.5) 集合的基本属性与操作
集合支持若干基础操作,使它在数据处理中非常有用:
# 检查唯一元素的数量
website_visitors = {"alice", "bob", "charlie", "alice", "david"}
print(f"Unique visitors: {len(website_visitors)}")
# Output: Unique visitors: 4
# 使用 'in' 检查成员关系(对集合来说非常快)
if "alice" in website_visitors:
print("Alice visited the website")
# Output: Alice visited the website
# 检查非成员关系
if "eve" not in website_visitors:
print("Eve has not visited yet")
# Output: Eve has not visited yet使用 in 进行成员测试是集合的关键优势之一。对于大型集合,在集合中检查某个元素是否存在会比在列表中检查快得多。我们会在 17.5 节中进一步探讨为什么这很重要。
17.2) 向集合添加与删除元素
与元组(tuple)(不可变)不同,集合是可变的——你可以在创建后添加和移除元素。不过,集合中的元素本身必须是不可变类型(我们会在 17.7 节中探讨这一限制)。
17.2.1) 使用 add() 添加单个元素
使用 add() 方法向集合中添加单个元素很直接。如果元素已存在,集合保持不变——不会抛出错误,也不会创建重复项:
# 构建已完成任务的集合
completed_tasks = {"task1", "task2"}
print(completed_tasks) # Output: {'task1', 'task2'}
# 添加一个新任务
completed_tasks.add("task3")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}
# 添加重复项不会产生影响
completed_tasks.add("task1")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}这种行为使集合非常适合用于跟踪唯一出现次数。你可以安全地调用 add() 而无需先检查元素是否存在——集合会自动处理重复项。
17.2.2) 使用 update() 添加多个元素
要一次添加多个元素,使用 update(),它接受任何可迭代对象(列表、元组、另一个集合等),并将其中所有元素添加到集合中:
# 从一个较小的技能集合开始
skills = {"Python", "SQL"}
print(skills) # Output: {'Python', 'SQL'}
# 从列表中添加多个技能
new_skills = ["JavaScript", "Docker", "Python"]
skills.update(new_skills)
print(skills) # Output: {'Python', 'SQL', 'JavaScript', 'Docker'}注意 "Python" 同时出现在原集合与被添加的列表中,但集合依然只包含一份副本。update() 方法也可以接收多个可迭代对象作为参数:
# 合并来自多个来源的技能
current_skills = {"Python"}
course_skills = ["JavaScript", "HTML"]
job_requirements = {"SQL", "Python", "Docker"}
current_skills.update(course_skills, job_requirements)
print(current_skills)
# Output: {'Python', 'JavaScript', 'HTML', 'SQL', 'Docker'}17.2.3) 使用 remove() 删除元素
删除元素需要小心。remove() 方法会从集合中删除一个元素,但如果元素不存在,会抛出 KeyError:
# 管理活跃用户
active_users = {"alice", "bob", "charlie", "david"}
# 移除一个已登出的用户
active_users.remove("bob")
print(active_users) # Output: {'alice', 'charlie', 'david'}
# 尝试移除不存在的元素会导致错误
# active_users.remove("eve") # Raises: KeyError: 'eve'由于 remove() 在元素缺失时会抛错,所以它最适合用于你确信元素存在的情况,或者当你希望在不存在时捕获该错误:
# 通过错误处理来安全移除(我们会在第 28 章学习更多 try/except)
users = {"alice", "bob", "charlie"}
user_to_remove = "david"
if user_to_remove in users:
users.remove(user_to_remove)
print(f"Removed {user_to_remove}")
else:
print(f"{user_to_remove} was not in the set")
# Output: david was not in the set17.2.4) 使用 discard() 安全地删除元素
为了更安全且不抛出错误的删除方式,discard() 提供了一个更宽容的替代方案。它会在元素存在时移除;如果元素不存在,则什么也不做:
# 管理购物车
cart_items = {"apple", "banana", "orange"}
# 安全地移除物品(即使物品不存在也不会报错)
cart_items.discard("banana")
print(cart_items) # Output: {'apple', 'orange'}
cart_items.discard("grape") # No error, even though grape isn't in the set
print(cart_items) # Output: {'apple', 'orange'}当你希望确保某个元素不在集合中(无论它一开始是否存在)时,使用 discard()。当元素的缺失意味着你想捕获的错误条件时,使用 remove()。
17.2.5) 使用 pop() 删除并返回一个任意元素
pop() 方法会从集合中移除并返回一个任意元素。由于集合是无序的,你无法预测会移除哪个元素:
# 处理待办任务队列(顺序不重要)
pending_tasks = {"email", "report", "meeting", "review"}
# 处理一个任务(我们不在意是哪一个)
task = pending_tasks.pop()
print(f"Processing: {task}") # Output: Processing: email (or another task)
print(f"Remaining: {pending_tasks}")
# Output: Remaining: {'report', 'meeting', 'review'} (without the popped task)如果你对空集合调用 pop(),会抛出 KeyError:
empty_set = set()
# empty_set.pop() # Raises: KeyError: 'pop from an empty set'当你需要处理集合中的所有元素但不关心顺序时,pop() 方法很有用:
# 处理集合中的所有元素
items_to_process = {"item1", "item2", "item3"}
while items_to_process:
item = items_to_process.pop()
print(f"Processing {item}")
# 处理该 item...
print("All items processed")
# Output:
# Processing item1
# Processing item2
# Processing item3
# All items processed17.2.6) 使用 clear() 删除所有元素
clear() 方法会移除集合中的所有元素,使其变为空集合:
# 重置会话数据
session_data = {"user_id", "timestamp", "ip_address"}
print(session_data) # Output: {'user_id', 'timestamp', 'ip_address'}
session_data.clear()
print(session_data) # Output: set()
print(len(session_data)) # Output: 0如果你想复用同一个集合对象,这比创建一个新的空集合更高效。
17.3) 集合运算:并集、交集、差集与对称差集
集合支持数学上的集合运算,让你能够高效地合并、比较与分析多个集合。这些操作是集合论的基础,并在数据处理中有许多实际应用。
17.3.1) 并集:合并集合
我们先从一个实际场景开始,理解并集为何重要。想象你正在管理不同课程的学生选课情况:
# 不同课程的选课学生
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# 找出选择任一课程(或两者都选)的所有学生
all_students = python_students | javascript_students
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}两个集合的并集(union)包含出现在任一集合(或两者)中的所有元素。Python 提供两种方式来计算并集:| 运算符(如上所示)和 union() 方法:
# 使用 union() 方法得到相同结果
all_students = python_students.union(javascript_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}union() 方法可以接收多个集合作为参数,这使它在合并多个来源的数据时非常方便:
# 三门不同课程的学生
python_students = {"alice", "bob"}
javascript_students = {"bob", "charlie"}
sql_students = {"charlie", "david"}
# 所有课程的全部学生
all_students = python_students.union(javascript_students, sql_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david'}并集的另一个例子是合并不同部门的邮件列表:
# 合并不同部门的邮件列表
marketing_contacts = {"alice@company.com", "bob@company.com"}
sales_contacts = {"bob@company.com", "charlie@company.com"}
support_contacts = {"david@company.com", "alice@company.com"}
# 跨部门的所有唯一联系人
all_contacts = marketing_contacts | sales_contacts | support_contacts
print(f"Total unique contacts: {len(all_contacts)}")
# Output: Total unique contacts: 417.3.2) 交集:找出共同元素
理解哪些元素同时出现在多个集合中,对许多数据分析任务都至关重要。交集(intersection)操作回答的问题是:“这些集合有什么共同点?”
# 找出同时购买两种产品的客户
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# 同时购买两种产品的客户
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}交集只包含同时出现在两个集合中的元素。你也可以使用 intersection() 方法,它可以接收多个集合:
# 找出同时选了三门课程的学生
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "charlie", "david"}
sql_students = {"charlie", "eve", "bob"}
# 同时选了三门课的学生
all_three = python_students.intersection(javascript_students, sql_students)
print(all_three) # Output: {'bob', 'charlie'}下面是一个实际用例:找出多个仓库都能供货的产品:
# 找出多个仓库都有的产品
warehouse_a = {"laptop", "mouse", "keyboard", "monitor"}
warehouse_b = {"mouse", "keyboard", "printer", "scanner"}
warehouse_c = {"keyboard", "monitor", "mouse", "desk"}
# 所有仓库都可用的产品
available_everywhere = warehouse_a & warehouse_b & warehouse_c
print(f"Available in all locations: {available_everywhere}")
# Output: Available in all locations: {'mouse', 'keyboard'}17.3.3) 差集:找出只在一个集合而不在另一个集合中的元素
有时候你需要识别某个集合中特有的内容。差集(difference)操作会找出只在第一个集合中、但不在第二个集合中的元素:
# 库存管理:找出差异
expected_items = {"item001", "item002", "item003", "item004"}
actual_items = {"item001", "item003", "item005"}
# 库存中缺少的物品
missing = expected_items - actual_items
print(f"Missing items: {missing}")
# Output: Missing items: {'item002', 'item004'}
# 库存中意外出现的物品
unexpected = actual_items - expected_items
print(f"Unexpected items: {unexpected}")
# Output: Unexpected items: {'item005'}你也可以使用 difference() 方法:
# 只在 Python 课程中的学生(不在 JavaScript 课程中)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
python_only = python_students.difference(javascript_students)
print(python_only) # Output: {'alice', 'charlie'}重要:差集运算不满足交换律——顺序很重要:
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# 在 Python 中但不在 JavaScript 中的学生
python_only = python_students - javascript_students
print(f"Python only: {python_only}")
# Output: Python only: {'alice', 'charlie'}
# 在 JavaScript 中但不在 Python 中的学生
javascript_only = javascript_students - python_students
print(f"JavaScript only: {javascript_only}")
# Output: JavaScript only: {'david', 'eve'}17.3.4) 对称差集:只在其中一个集合中出现、但不同时在两者中出现的元素
对称差集(symmetric difference)会找出只出现在其中一个集合里、但不同时出现在两个集合里的元素。这个操作对识别两个版本之间的变化尤其有用:
# 对比配置的两个版本
old_settings = {"debug", "logging", "cache", "compression"}
new_settings = {"logging", "cache", "monitoring", "security"}
# 发生变化的配置项(新增或移除)
changes = old_settings ^ new_settings
print(f"Changed settings: {changes}")
# Output: Changed settings: {'debug', 'compression', 'monitoring', 'security'}
# 若要明确区分新增与移除:
removed = old_settings - new_settings
added = new_settings - old_settings
print(f"Removed: {removed}") # Output: Removed: {'debug', 'compression'}
print(f"Added: {added}") # Output: Added: {'monitoring', 'security'}你也可以使用 symmetric_difference() 方法:
# 只选了一门课程的学生(不是两门都选)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
one_course_only = python_students.symmetric_difference(javascript_students)
print(one_course_only)
# Output: {'alice', 'charlie', 'david', 'eve'}与差集不同,对称差集满足交换律——顺序不重要:
result1 = python_students ^ javascript_students
result2 = javascript_students ^ python_students
print(result1 == result2) # Output: True17.4) 子集与超集关系(issubset、issuperset、isdisjoint)
除了合并集合之外,我们还经常需要理解集合之间的关系。Python 提供了方法来测试一个集合是否被另一个集合包含、是否包含另一个集合,或是否与另一个集合完全没有共同元素。
17.4.1) 使用 issubset() 和 <= 测试子集
如果集合 A 的每个元素也都在集合 B 中,那么 A 是 B 的子集(subset)。换句话说,B 包含 A 的全部元素(并可能还有更多)。
# 课程先修要求
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# 检查 basic_skills 是否为 intermediate_skills 的子集
print(basic_skills.issubset(intermediate_skills)) # Output: True
print(basic_skills <= intermediate_skills) # Output: True (same result)一个集合总是它自身的子集:
skills = {"Python", "SQL", "JavaScript"}
print(skills.issubset(skills)) # Output: True
print(skills <= skills) # Output: True如果你想测试真子集(proper subset)(A 是 B 的子集但不等于 B),使用 < 运算符:
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# 真子集:basic 是 intermediate 的子集,并且二者不相等
print(basic_skills < intermediate_skills) # Output: True
# 不是自身的真子集(它们相等)
print(basic_skills < basic_skills) # Output: False子集测试的一个实际例子是检查权限或需求:
# 用户权限系统
required_permissions = {"read", "write"}
user_permissions = {"read", "write", "delete", "admin"}
# 检查用户是否拥有所有必需权限
if required_permissions.issubset(user_permissions):
print("Access granted")
else:
print("Access denied - missing permissions")
# Output: Access granted
# 另一个权限不足的用户
limited_user = {"read"}
if required_permissions.issubset(limited_user):
print("Access granted")
else:
missing = required_permissions - limited_user
print(f"Access denied - missing: {missing}")
# Output: Access denied - missing: {'write'}17.4.2) 使用 issuperset() 和 >= 测试超集
如果集合 A 包含集合 B 的所有元素,那么 A 是 B 的超集(superset)。这是子集的逆关系——如果 A 是 B 的子集,那么 B 是 A 的超集。
# 技能等级
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis", "research"}
# 检查 advanced_skills 是否为 basic_skills 的超集
print(advanced_skills.issuperset(basic_skills)) # Output: True
print(advanced_skills >= basic_skills) # Output: True (same result)与子集一样,一个集合总是它自身的超集:
skills = {"Python", "SQL"}
print(skills.issuperset(skills)) # Output: True对于真超集(proper superset)(A 是 B 的超集但不等于 B),使用 > 运算符:
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis"}
# 真超集:advanced 包含 basic 的全部元素,并且还有更多
print(advanced_skills > basic_skills) # Output: True
# 不是自身的真超集
print(advanced_skills > advanced_skills) # Output: False17.4.3) 使用 isdisjoint() 测试互不相交的集合
如果两个集合没有任何共同元素——它们的交集为空,那么它们是互不相交(disjoint)的。isdisjoint() 方法在集合之间没有共享元素时返回 True:
# 检查排课是否有冲突
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "art", "music"}
# 检查是否有任何冲突(同一门课出现在两个时段)
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: No scheduling conflicts当集合不是互不相交时:
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "math", "music"}
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: Conflicts: {'math'}空集合与所有集合(包括其他空集合)都互不相交:
empty = set()
numbers = {1, 2, 3}
print(empty.isdisjoint(numbers)) # Output: True
print(empty.isdisjoint(empty)) # Output: True17.5) 何时使用集合而不是列表
理解何时使用集合与列表对编写高效的 Python 代码至关重要。虽然二者都用于存储元素集合,但它们的特性不同,因此适用于不同任务。
17.5.1) 使用集合进行快速成员测试
集合最显著的优势之一是在成员测试上的速度。检查某个元素是否存在于集合中,比在列表中检查要快得多,尤其是在大集合时:
# 检查用户是否在一个很大的集合中
active_users_list = []
for i in range(10000):
active_users_list.append("user" + str(i))
# 使用列表(对大集合来说较慢)
print("user5000" in active_users_list) # Checks each element until found
active_users_set = set()
for i in range(10000):
active_users_set.add("user" + str(i))
# 使用集合(无论大小都很快)
print("user5000" in active_users_set) # Direct lookup尽管二者产生相同的结果,但对于大型集合,集合版本会显著更快。这是因为集合内部使用哈希表(hash table),使得查找几乎瞬间完成且与大小无关;而列表必须按顺序逐个检查元素。
17.5.2) 使用集合去重
当你需要从集合中移除重复项时,转换为集合是最简单的方法:
# 从用户输入中移除重复项
survey_responses = [
"yes", "no", "yes", "maybe", "yes", "no", "maybe", "yes"
]
# 获取唯一回复
unique_responses = set(survey_responses)
print(unique_responses) # Output: {'yes', 'no', 'maybe'}
# 如果你需要再转回列表(去重后)
unique_list = list(unique_responses)
print(unique_list) # Output: ['yes', 'no', 'maybe'] (order may vary)17.5.3) 使用集合进行数学集合运算
当你需要找出多个集合之间的共同元素、差异或并集时,集合提供了清晰且高效的操作:
# 分析客户购买模式
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# 同时购买两种产品的客户
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}
# 只购买产品 A 的客户
only_a = customers_product_a - customers_product_b
print(f"Only product A: {only_a}")
# Output: Only product A: {101, 102}
# 至少购买一种产品的所有客户
all_customers = customers_product_a | customers_product_b
print(f"Total customers: {len(all_customers)}")
# Output: Total customers: 717.5.4) 当顺序重要时使用列表
集合是无序的,所以如果元素顺序很重要,你必须使用列表:
# WRONG - 使用集合无法保留顺序
task_order = {"wake up", "breakfast", "work", "lunch", "work", "dinner"}
print(task_order) # Order is unpredictable and "work" appears only once
# CORRECT - 当顺序重要时使用列表
task_order = ["wake up", "breakfast", "work", "lunch", "work", "dinner"]
print(task_order)
# Output: ['wake up', 'breakfast', 'work', 'lunch', 'work', 'dinner']17.5.5) 当重复项有意义时使用列表
如果重复值携带信息(如频率或多次出现),请使用列表:
# 记录测验分数(重复项表示有多少学生得到该分数)
quiz_scores = [85, 90, 85, 78, 90, 92, 85, 88]
# 使用列表,我们可以统计出现次数
score_85_count = quiz_scores.count(85)
print(f"Students who scored 85: {score_85_count}")
# Output: Students who scored 85: 3
# 使用集合则会丢失这类信息
unique_scores = set(quiz_scores)
print(unique_scores) # Output: {78, 85, 88, 90, 92}
# We can't tell how many students got each score17.5.6) 当你需要索引访问时使用列表
集合不支持索引,因为它是无序的。如果你需要按位置访问元素,请使用列表:
# WRONG - 集合不支持索引
colors = {"red", "blue", "green"}
# first_color = colors[0] # Raises: TypeError: 'set' object is not subscriptable
# CORRECT - 使用列表进行索引访问
colors = ["red", "blue", "green"]
first_color = colors[0]
print(first_color) # Output: red17.6) Frozensets 与不可变集合
到目前为止,我们使用的是普通集合,它是可变的——你可以在创建后添加或删除元素。Python 还提供了 frozenset(frozenset),它是集合的不可变版本。一旦创建,frozenset 就不能被修改。
17.6.1) 创建 Frozenset
你可以使用 frozenset() 构造器来创建 frozenset,类似于使用 set() 创建普通集合:
# 从列表创建 frozenset
colors = frozenset(["red", "blue", "green"])
print(colors) # Output: frozenset({'red', 'blue', 'green'})
print(type(colors)) # Output: <class 'frozenset'>
# 从元组创建 frozenset
numbers = frozenset((1, 2, 3, 4, 5))
print(numbers) # Output: frozenset({1, 2, 3, 4, 5})
# 创建空 frozenset
empty = frozenset()
print(empty) # Output: frozenset()与普通集合一样,frozenset 也会自动去除重复项:
# 重复项会被移除
values = frozenset([1, 2, 2, 3, 3, 3, 4])
print(values) # Output: frozenset({1, 2, 3, 4})17.6.2) Frozenset 是不可变的
一旦创建,你就无法修改 frozenset。像 add()、remove()、discard()、pop() 和 clear() 这类方法在 frozenset 上都不存在:
# 创建一个 frozenset
languages = frozenset(["Python", "JavaScript", "Java"])
# 尝试修改会报错
# languages.add("C++") # AttributeError: 'frozenset' object has no attribute 'add'
# languages.remove("Java") # AttributeError: 'frozenset' object has no attribute 'remove'不可变性是 frozenset 的定义性特征。如果你需要“修改”一个 frozenset,你必须创建一个新的:
# 原始 frozenset
original = frozenset([1, 2, 3])
# 创建包含额外元素的新 frozenset
modified = frozenset(list(original) + [4])
print(original) # Output: frozenset({1, 2, 3})
print(modified) # Output: frozenset({1, 2, 3, 4})17.6.3) 集合运算同样适用于 Frozenset
frozenset 支持与普通集合相同的所有集合运算(并集、交集、差集等):
# 使用 frozenset 进行集合运算
set_a = frozenset([1, 2, 3, 4])
set_b = frozenset([3, 4, 5, 6])
# Union
print(set_a | set_b) # Output: frozenset({1, 2, 3, 4, 5, 6})
# Intersection
print(set_a & set_b) # Output: frozenset({3, 4})
# Difference
print(set_a - set_b) # Output: frozenset({1, 2})
# Symmetric difference
print(set_a ^ set_b) # Output: frozenset({1, 2, 5, 6})你也可以在运算中混用普通集合与 frozenset:
regular_set = {1, 2, 3}
frozen_set = frozenset([3, 4, 5])
# 普通集合与 frozen_set 之间的运算
result = regular_set | frozen_set
print(result) # Output: {1, 2, 3, 4, 5}
print(type(result)) # Output: <class 'set'> (result is a regular set)17.6.4) 为什么要使用 Frozenset?
使用 frozenset 的主要原因是:它可以作为字典的键或作为其他集合的元素,而普通集合不行:
# WRONG - 普通集合不能作为字典键
# regular_set = {1, 2, 3}
# my_dict = {regular_set: "value"} # TypeError: unhashable type: 'set'
# CORRECT - frozenset 可以作为字典键
frozen_set = frozenset([1, 2, 3])
my_dict = {frozen_set: "value"}
print(my_dict) # Output: {frozenset({1, 2, 3}): 'value'}
print(my_dict[frozen_set]) # Output: value一个使用 frozenset 作为字典键的实际例子:
# 存储关于坐标对的信息
# 每个坐标都是 (x, y) 值组成的 frozenset
location_data = {
frozenset([0, 0]): "origin",
frozenset([1, 0]): "east",
frozenset([1, 1]): "northeast"
}
# 查找位置
point = frozenset([1, 0])
print(location_data[point]) # Output: eastfrozenset 也可以作为其他集合的元素:
# WRONG - 普通集合不能作为集合的元素
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# CORRECT - frozenset 可以作为集合的元素
set_of_frozensets = {
frozenset([1, 2]),
frozenset([3, 4]),
frozenset([5, 6])
}
print(set_of_frozensets)
# Output: {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}一个表示分组的实际例子:
# 表示队伍:每个队伍是由玩家 ID 组成的 frozenset
tournament_teams = {
frozenset([101, 102, 103]), # Team A
frozenset([201, 202, 203]), # Team B
frozenset([301, 302, 303]) # Team C
}
# 检查某支队伍是否已注册
team_to_check = frozenset([101, 102, 103])
if team_to_check in tournament_teams:
print("Team is registered")
else:
print("Team not found")
# Output: Team is registered17.6.5) 在集合与 Frozenset 之间转换
你可以很容易地在普通集合与 frozenset 之间转换:
# 将普通集合转换为 frozenset
regular = {1, 2, 3, 4}
frozen = frozenset(regular)
print(frozen) # Output: frozenset({1, 2, 3, 4})
# 将 frozenset 转换为普通集合
frozen = frozenset([5, 6, 7, 8])
regular = set(frozen)
print(regular) # Output: {5, 6, 7, 8}
# 现在我们可以修改普通集合
regular.add(9)
print(regular) # Output: {5, 6, 7, 8, 9}17.7) 可哈希与不可哈希类型:什么可以作为字典键或集合元素(以及关于哈希的一点说明)
在本章中,我们已经看到集合可以包含某些类型的对象,但不能包含其他类型。例如,你可以创建一个整数或字符串的集合,但不能创建一个列表的集合。这一限制之所以存在,是因为集合元素(以及字典键,正如我们在第 16 章学到的)必须是可哈希(hashable)的。
17.7.1) “可哈希”是什么意思?
一个可哈希(hashable)对象是指:它拥有一个在其生命周期内永远不变的哈希值。Python 通过一个名为 hash() 的内置函数来计算这个哈希值:
# 可哈希类型拥有哈希值
print(hash(42)) # Output: 42
print(hash("Python")) # Output: (some large integer)
print(hash((1, 2, 3))) # Output: (some large integer)哈希值是一个整数,Python 在内部用它来快速定位集合与字典中的对象。你可以把它理解成一个地址或索引,帮助 Python 高效地找到对象。
关键特性:要让一个对象可哈希,它的哈希值必须在其生命周期内保持不变。这意味着对象本身必须是不可变的——如果对象能改变,它的哈希值也必须随之改变,这将破坏集合与字典。
17.7.2) 不可变类型是可哈希的
Python 的所有不可变内置类型都是可哈希的,因此可以作为集合元素或字典键:
# 整数是可哈希的
numbers = {1, 2, 3, 4, 5}
print(numbers) # Output: {1, 2, 3, 4, 5}
# 字符串是可哈希的
words = {"apple", "banana", "cherry"}
print(words) # Output: {'apple', 'banana', 'cherry'}
# 元组是可哈希的(前提是它只包含可哈希元素)
coordinates = {(0, 0), (1, 1), (2, 2)}
print(coordinates) # Output: {(0, 0), (1, 1), (2, 2)}
# frozenset 是可哈希的
frozen_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozen_sets) # Output: {frozenset({1, 2}), frozenset({3, 4})}
# 布尔值与 None 是可哈希的
mixed = {True, False, None, 42, "text"}
print(mixed) # Output: {False, True, None, 42, 'text'}17.7.3) 可变类型不可哈希
像列表、普通集合与字典这类可变类型不可哈希,因为它们的内容可以改变:
# 列表不是可哈希的
# my_set = {[1, 2, 3]} # TypeError: unhashable type: 'list'
# 普通集合不是可哈希的
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# 字典不是可哈希的
# my_set = {{"key": "value"}} # TypeError: unhashable type: 'dict'为什么可变性很关键?想象一下如果我们可以把列表加入集合,会发生什么:
# 假设场景(这实际上并不能运行)
# my_list = [1, 2, 3]
# my_set = {my_list} # Suppose this worked
#
# # Python computes hash based on [1, 2, 3]
# # Now we modify the list:
# my_list.append(4) # Now it's [1, 2, 3, 4]
#
# # The hash value would be wrong! The set would be corrupted.这就是为什么 Python 会阻止可变对象出现在集合中或作为字典键——否则会破坏内部数据结构。
初学者常见困惑:尽管集合本身是可变的(你可以添加和删除元素),但集合中的元素必须是不可变的。初学者有时会在把对象添加进集合后尝试修改它们,却没有意识到这个概念上的区别:
# 常见困惑:set 可变,但元素必须不可变
# set 是可变的——你可以改变其中的内容
fruits = {'apple', 'banana'}
fruits.add('orange') # ✓ Works
fruits.remove('apple') # ✓ Works
# 但元素必须不可变——它们不能被修改
my_list = [1, 2, 3]
# my_set = {my_list} # ✗ TypeError: unhashable type: 'list'
# Why? If you could modify my_list after adding it, the set's internal
# structure would be corrupted.
# 这之所以可行,是因为元组不可变
my_tuple = (1, 2, 3)
my_set = {my_tuple} # ✓ Works - tuples can't be modified17.7.4) 元组的特殊情况
只有当元组的所有元素都是可哈希时,元组才是可哈希的。包含可变对象的元组不可哈希:
# 仅包含不可变元素的元组——可哈希
good_tuple = (1, 2, "three")
my_set = {good_tuple} # Works: good_tuple is hashable
print(my_set) # Output: {(1, 2, 'three')}
# 包含列表的元组——不可哈希
bad_tuple = (1, 2, [3, 4])
# my_set = {bad_tuple} # TypeError: unhashable type: 'list'这很合理:尽管元组本身不可变(你不能改变它包含哪些对象),但如果其中某个对象是可变的,那么元组整体的“值”就可能改变:
# 演示为什么包含可变元素的元组不能被哈希
inner_list = [1, 2]
my_tuple = (inner_list, 3)
# 元组结构固定,但里面的列表可以变化
inner_list.append(3) # Now inner_list is [1, 2, 3]
# 元组现在“包含”了不同的数据,但它仍然是同一个元组对象17.7.5) 测试可哈希性
你可以通过尝试计算对象的哈希值来测试它是否可哈希:
# 测试可哈希性
def is_hashable(obj):
"""检查对象是否可哈希。"""
try:
hash(obj)
return True
except TypeError:
return False
# 测试各种类型
print(is_hashable(42)) # Output: True
print(is_hashable("text")) # Output: True
print(is_hashable((1, 2, 3))) # Output: True
print(is_hashable([1, 2, 3])) # Output: False
print(is_hashable({1, 2, 3})) # Output: False
print(is_hashable({"key": "value"})) # Output: False17.7.6) 可哈希类型汇总
可哈希(可以作为集合元素或 dict key):
- 整数:
42 - 浮点数:
3.14 - 字符串:
"text" - 元组(如果所有元素都可哈希):
(1, 2, "three") - Frozenset:
frozenset([1, 2, 3]) - 布尔值:
True、False - None:
None
不可哈希(不能作为集合元素或 dict key):
- 列表:
[1, 2, 3] - 普通集合:
{1, 2, 3} - 字典:
{"key": "value"} - 包含不可哈希元素的元组:
(1, [2, 3])
理解可哈希性能够帮助你选择正确的数据结构,并在使用集合与字典时避免常见错误。关键原则很简单:如果对象可以改变,它就不能被哈希;如果它不能被哈希,它就不能出现在集合中,也不能作为字典键。