Python基础指南之集合的四种基本运算详解
作者:星河耀银海
一、开篇:把集合当成数学里的集合来用
前一篇文章我们学会了集合的创建和去重。但集合真正发光发热的地方,其实在于它的集合运算——交、并、差、对称差。这些概念和你在中学数学里学的"集合论"一脉相承,但用Python代码表达出来,代码量少得惊人。
看一个实际场景:你有两个用户列表——昨天活跃的用户和今天活跃的用户。你想知道:哪些用户连续两天都活跃了(交集)?哪些用户昨天活跃但今天不活跃了(差集)?所有活跃过的用户有哪些(并集)?用集合来做这些分析,几行代码就搞定,而且因为基于哈希表实现,速度极快。
yesterday = {'alice', 'bob', 'charlie', 'david'}
today = {'alice', 'charlie', 'eve', 'frank'}
# 连续两天活跃(交集)
loyal = yesterday & today
print(f'忠实用户: {loyal}') # {'alice', 'charlie'}
# 昨天活跃今天不活跃(差集)
churned = yesterday - today
print(f'流失用户: {churned}') # {'bob', 'david'}
# 今天新活跃(差集)
new_users = today - yesterday
print(f'新用户: {new_users}') # {'eve', 'frank'}
# 所有活跃过的用户(并集)
all_users = yesterday | today
print(f'全部用户: {all_users}') # {'alice', 'bob', 'charlie', 'david', 'eve', 'frank'}
这篇文章会把四种集合运算讲透:每种运算的运算符和对应方法、使用场景、性能特点,以及它们在真实项目中的实战应用。
二、并集(Union)
2.1 基本概念
并集操作返回两个(或多个)集合中的所有元素,自动去重。数学符号是 ∪,Python里有两种写法:| 运算符和 union() 方法。
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# 方式一:| 运算符(推荐)
result = a | b
print(result) # {1, 2, 3, 4, 5, 6}
# 方式二:union() 方法
result = a.union(b)
print(result) # {1, 2, 3, 4, 5, 6}
# 两者的区别:
# - | 运算符两边都必须是集合
# - union() 方法可以接受任何可迭代对象
2.2 union()方法的优势:接受任意可迭代对象
a = {1, 2, 3}
# | 运算符——两边都必须是集合
result1 = a | {4, 5} # ✅ 可以
# result2 = a | [4, 5] # ❌ TypeError——列表不能|集合
# union()——参数可以是任何可迭代对象
result3 = a.union([4, 5]) # ✅ 列表
result4 = a.union((4, 5)) # ✅ 元组
result5 = a.union(range(4, 7)) # ✅ range
result6 = a.union('hello') # ✅ 字符串——{'h', 'e', 'l', 'o', 1, 2, 3}
result7 = a.union({4: 'a', 5: 'b'}) # ✅ 字典——取键
print(f'并集(list): {result3}')
print(f'并集(tuple): {result4}')
print(f'并集(range): {result5}')
print(f'并集(str): {result6}')
2.3 多个集合的并集
a = {1, 2}
b = {2, 3}
c = {3, 4}
d = {4, 5}
# | 运算符链式使用
result = a | b | c | d
print(result) # {1, 2, 3, 4, 5}
# union() 可以同时接受多个参数
result = a.union(b, c, d)
print(result) # {1, 2, 3, 4, 5}
# 对空集合求并
result = set().union([1, 2, 3], [3, 4, 5], range(5, 8))
print(result) # {1, 2, 3, 4, 5, 6, 7}
2.4 并集的原地操作:|=
# |= 是原地并集——修改调用方集合
a = {1, 2, 3}
b = {3, 4, 5}
original_id = id(a)
a |= b # 等价于 a.update(b)
print(a) # {1, 2, 3, 4, 5}
print(f'是否同一个对象: {id(a) == original_id}') # True——原地修改
# 对比:a = a | b 会创建新对象
a = {1, 2, 3}
original_id = id(a)
a = a | b # 创建新集合
print(f'是否同一个对象: {id(a) == original_id}') # False——新对象
2.5 并集实战:合并多个数据源
# 场景:从多个渠道收集用户邮箱,合并去重
email_sources = {
'web_registration': {'alice@test.com', 'bob@test.com', 'charlie@test.com'},
'mobile_app': {'bob@test.com', 'david@test.com', 'eve@test.com'},
'api_import': {'charlie@test.com', 'frank@test.com', 'grace@test.com'},
'manual_entry': {'alice@test.com', 'henry@test.com'},
}
# 合并所有渠道的邮箱
all_emails = set().union(*email_sources.values())
print(f'总邮箱数: {len(all_emails)}')
print(f'邮箱列表: {all_emails}')
# 分析每个渠道的贡献
for source, emails in email_sources.items():
contribution = len(emails)
unique_to_source = len(emails - all_emails) # 这个渠道独有的
print(f'{source}: {contribution}个邮箱, 其中独有{unique_to_source}个')
# 更简洁的方式——用 | 运算符
all_emails2 = set()
for emails in email_sources.values():
all_emails2 |= emails
print(f'\n用|=合并: {len(all_emails2)}个邮箱')
三、交集(Intersection)
3.1 基本概念
交集返回两个(或多个)集合中共有的元素。数学符号是 ∩,Python里有两种写法:& 运算符和 intersection() 方法。
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# 方式一:& 运算符
result = a & b
print(result) # {3, 4}
# 方式二:intersection() 方法
result = a.intersection(b)
print(result) # {3, 4}
# 同样,& 要求两边都是集合
# intersection() 可以接受任何可迭代对象
result = a.intersection([3, 4, 5]) # ✅ 列表也行
print(result) # {3, 4}
3.2 多个集合的交集
a = {1, 2, 3, 4, 5}
b = {2, 3, 4, 5, 6}
c = {3, 4, 5, 6, 7}
d = {4, 5, 6, 7, 8}
# & 运算符链式使用
result = a & b & c & d
print(result) # {4, 5}
# intersection() 接受多个参数
result = a.intersection(b, c, d)
print(result) # {4, 5}
# 求多个列表的公共元素
lists = [
[1, 2, 3, 4, 5],
[2, 3, 4, 5, 6],
[3, 4, 5, 6, 7],
]
# 转为集合后求交集
common = set(lists[0]).intersection(*lists[1:])
print(f'所有列表的公共元素: {common}') # {3, 4, 5}
3.3 交集的原地操作:&=
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a &= b # 等价于 a.intersection_update(b)
print(a) # {3, 4}——a被修改了
3.4 交集实战:找出共同特征
# 场景:找出同时满足多个条件的用户
# 不同条件下的用户集合
purchased_last_month = {'alice', 'bob', 'charlie', 'david', 'eve'}
subscribed_newsletter = {'alice', 'charlie', 'frank', 'grace'}
left_positive_review = {'alice', 'david', 'frank', 'henry'}
premium_member = {'alice', 'bob', 'charlie', 'grace', 'henry'}
# 找出"最忠实"的用户——同时满足所有条件
most_loyal = (
purchased_last_month
& subscribed_newsletter
& left_positive_review
& premium_member
)
print(f'四项全满足的用户: {most_loyal}') # {'alice'}
# 找出"上月购买 + 好评"的用户
quality_buyers = purchased_last_month & left_positive_review
print(f'购买并好评的用户: {quality_buyers}') # {'alice', 'david'}
# 找出"订阅了但没好评"的用户
subscribed_no_review = subscribed_newsletter - left_positive_review
print(f'订阅但未好评: {subscribed_no_review}') # {'charlie', 'grace'}
# 逐步缩小范围
step1 = purchased_last_month & subscribed_newsletter
print(f'第1步-购买且订阅: {step1}')
step2 = step1 & left_positive_review
print(f'第2步-再加上好评: {step2}')
step3 = step2 & premium_member
print(f'第3步-再加上会员: {step3}') # 最终结果
四、差集(Difference)
4.1 基本概念
差集返回在前一个集合但不在后一个集合中的元素。数学符号是 −,Python里有两种写法:- 运算符和 difference() 方法。
重要:差集是不对称的!a - b ≠ b - a。
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}
# a - b:在a中但不在b中的元素
print(a - b) # {1, 2, 3}
# b - a:在b中但不在a中的元素
print(b - a) # {6, 7, 8}
# difference() 方法
print(a.difference(b)) # {1, 2, 3}
# difference()也可以接受可迭代对象
print(a.difference([4, 5])) # {1, 2, 3}
4.2 多个集合的差集
a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b = {1, 2, 3}
c = {4, 5}
d = {7, 8, 9}
# 从a中依次减去b、c、d
result = a - b - c - d
print(result) # {6, 10}
# difference()接受多个参数——等价于连续求差
result = a.difference(b, c, d)
print(result) # {6, 10}
# ⚠️ 注意:a - b - c 的执行顺序是 ((a - b) - c)
# difference(b, c, d) 等价于 a - b - c - d
4.3 差集的原地操作:-=
a = {1, 2, 3, 4, 5}
remove_set = {2, 4}
a -= remove_set # 等价于 a.difference_update(remove_set)
print(a) # {1, 3, 5}
4.4 差集实战:过滤和排除
# 场景一:权限管理——排除被禁用的操作
all_permissions = {'read', 'write', 'delete', 'admin', 'export', 'import'}
role_admin = {'read', 'write', 'delete', 'admin', 'export', 'import'}
role_editor = {'read', 'write', 'export'}
role_viewer = {'read', 'export'}
forbidden_for_editor = {'delete', 'admin', 'import'}
# 验证editor的实际权限
editor_actual = role_editor - forbidden_for_editor
print(f'Editor实际权限: {editor_actual}') # {'read', 'write', 'export'}
# admin也有被禁的操作吗?
admin_actual = role_admin & forbidden_for_editor
print(f'Admin被限制的操作: {admin_actual}') # {'delete', 'admin', 'import'}——admin不受限
# 场景二:内容审核——过滤敏感词
content_words = {'这件', '商品', '非常', '好', '用', '推荐', '购买'}
sensitive_words = {'推荐', '购买', '免费', '优惠'}
safe_words = content_words - sensitive_words
print(f'安全词汇: {safe_words}') # {'这件', '商品', '非常', '好', '用'}
# 发现被过滤的词
filtered_words = content_words & sensitive_words
print(f'被过滤的词: {filtered_words}') # {'推荐', '购买'}
# 场景三:数据增量更新——找出需要新增和删除的数据
old_data = {'user_001', 'user_002', 'user_003', 'user_004', 'user_005'}
new_data = {'user_002', 'user_003', 'user_005', 'user_006', 'user_007'}
to_add = new_data - old_data
to_delete = old_data - new_data
to_keep = old_data & new_data
print(f'需要新增: {to_add}') # {'user_006', 'user_007'}
print(f'需要删除: {to_delete}') # {'user_001', 'user_004'}
print(f'保持不变: {to_keep}') # {'user_002', 'user_003', 'user_005'}
五、对称差集(Symmetric Difference)
5.1 基本概念
对称差集返回只在其中一个集合中出现的元素(排除了两个集合都有的元素)。数学符号是 Δ 或 ⊕,Python里有两种写法:^ 运算符和 symmetric_difference() 方法。
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# 对称差集——在a或b中,但不同时在两者中
result = a ^ b
print(result) # {1, 2, 5, 6}
# symmetric_difference() 方法
result = a.symmetric_difference(b)
print(result) # {1, 2, 5, 6}
# 数学上:a ^ b = (a - b) | (b - a) = (a | b) - (a & b)
print(f'a ^ b == (a - b) | (b - a): {a ^ b == (a - b) | (b - a)}') # True
print(f'a ^ b == (a | b) - (a & b): {a ^ b == (a | b) - (a & b)}') # True
5.2 对称差集的特点
# 对称差集是对称的:a ^ b == b ^ a
a = {1, 2, 3}
b = {2, 3, 4}
print(a ^ b) # {1, 4}
print(b ^ a) # {1, 4}
print(a ^ b == b ^ a) # True
# ⚠️ 注意:symmetric_difference()只接受一个参数
# a.symmetric_difference(b, c) # TypeError——不接受多个参数
# 多个集合的对称差可以用^链式
c = {3, 4, 5}
result = a ^ b ^ c
print(result) # 结果取决于计算顺序
# 如果想计算"只在恰好一个集合中出现的元素"
# symmetric_difference不直接支持,但可以组合出来
a = {1, 2, 3}
b = {2, 3, 4}
c = {3, 4, 5}
# 恰好在恰好一个集合中(用对称差不够,要用更精确的计算)
in_exactly_one = (a - b - c) | (b - a - c) | (c - a - b)
print(f'恰好在恰好一个集合中: {in_exactly_one}') # {1, 5}
5.3 对称差集的原地操作:^=
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a ^= b # 等价于 a.symmetric_difference_update(b)
print(a) # {1, 2, 5, 6}
5.4 对称差集实战:找出差异
# 场景一:找出两个版本间的变更
version_1_features = {'login', 'register', 'profile', 'search', 'cart'}
version_2_features = {'login', 'register', 'profile', 'search', 'cart', 'chat', 'analytics'}
# 对称差——所有发生了变化的功能(新增或删除)
changes = version_1_features ^ version_2_features
print(f'变更的功能: {changes}') # {'chat', 'analytics'}
# 进一步区分新增和删除
added = version_2_features - version_1_features
removed = version_1_features - version_2_features
print(f'新增: {added}') # {'chat', 'analytics'}
print(f'删除: {removed}') # set()——没有删除
# 场景二:找出两个用户标签集的差异
user1_tags = {'Python', 'Web', 'Django', 'JavaScript', 'React'}
user2_tags = {'Python', '数据', '机器学习', 'JavaScript', 'Vue'}
# 共同标签
common_tags = user1_tags & user2_tags
print(f'共同标签: {common_tags}') # {'Python', 'JavaScript'}
# 不同标签(对称差)
different_tags = user1_tags ^ user2_tags
print(f'不同标签: {different_tags}') # {'Web', 'Django', 'React', '数据', '机器学习', 'Vue'}
# 各用户独有标签
user1_unique = user1_tags - user2_tags
user2_unique = user2_tags - user1_tags
print(f'用户1独有: {user1_unique}') # {'Web', 'Django', 'React'}
print(f'用户2独有: {user2_unique}') # {'数据', '机器学习', 'Vue'}
# 验证:对称差 = 独有标签的并集
print(f'验证: {different_tags == user1_unique | user2_unique}') # True
六、运算符 vs 方法:如何选择
6.1 全面对比
| 运算 | 运算符 | 方法 | 原地运算符 | 原地方法 |
|---|---|---|---|---|
| 并集 | | | union(*others) | |= | update(*others) |
| 交集 | & | intersection(*others) | &= | intersection_update(*others) |
| 差集 | - | difference(*others) | -= | difference_update(*others) |
| 对称差 | ^ | symmetric_difference(other) | ^= | symmetric_difference_update(other) |
6.2 选择建议
# 使用运算符(| & - ^)的情况:
# 1. 两个操作数都是集合
# 2. 代码简洁性优先
# 3. 链式操作——a | b | c | d
# 使用方法的情况:
# 1. 操作数是列表、元组等非集合类型
# 2. 需要同时传入多个参数
# 3. 代码可读性优先(方法名比符号更直白)
set_a = {1, 2, 3}
list_b = [3, 4, 5]
tuple_c = (5, 6, 7)
# 方法接受可迭代对象——不需要转换
result = set_a.union(list_b, tuple_c)
print(result) # {1, 2, 3, 4, 5, 6, 7}
# 运算符——必须两边都是集合
# set_a | list_b # TypeError
result = set_a | set(list_b) | set(tuple_c)
print(result) # {1, 2, 3, 4, 5, 6, 7}
6.3 原地操作 vs 创建新对象
a = {1, 2, 3}
b = {3, 4, 5}
# 创建新对象(推荐——不会意外修改原数据)
c = a | b
print(f'a: {a}, c: {c}') # a不变
# 原地修改(谨慎使用——修改了原始数据)
a |= b
print(f'a: {a}') # a被修改了
# 💡 建议:除非你有明确的性能需求(如大数据量、循环中),
# 否则优先使用创建新对象的方式(代码更安全、更易理解)
七、集合运算的性能特性
7.1 时间复杂度
集合的所有基本运算基于哈希表实现,效率极高:
| 运算 | 时间复杂度 | 说明 |
|---|---|---|
add(x) | O(1) | 哈希表插入 |
remove(x) | O(1) | 哈希表删除 |
x in s | O(1) | 哈希表查找 |
a | b | O(len(a) + len(b)) | 遍历两个集合 |
a & b | O(min(len(a), len(b))) | 遍历较小的集合 |
a - b | O(len(a)) | 遍历a,检查是否在b中 |
a ^ b | O(len(a) + len(b)) | 等价于 (a|b) - (a&b) |
7.2 性能对比:集合 vs 列表
import time
# 准备测试数据
n = 10000
list_a = list(range(n))
list_b = list(range(n // 2, n + n // 2))
set_a = set(list_a)
set_b = set(list_b)
# 并集
start = time.perf_counter()
for _ in range(1000):
_ = set_a | set_b
print(f'集合并集: {time.perf_counter() - start:.4f}s')
# 用列表模拟并集(去重)
start = time.perf_counter()
for _ in range(10): # 只跑10次——太慢了
_ = list(set(list_a + list_b))
print(f'列表"并集": {time.perf_counter() - start:.4f}s (仅10次)')
# 交集
start = time.perf_counter()
for _ in range(1000):
_ = set_a & set_b
print(f'集合交集: {time.perf_counter() - start:.4f}s')
# 用列表模拟交集
start = time.perf_counter()
for _ in range(10):
_ = [x for x in list_a if x in list_b]
print(f'列表"交集": {time.perf_counter() - start:.4f}s (仅10次)')
# 集合运算通常比等价的列表操作快100-1000倍
八、综合实战
8.1 实战一:好友推荐系统
# 场景:社交网络的好友推荐
# 推荐规则:如果A和B不是好友,但他们有很多共同好友,则推荐B给A
class FriendRecommender:
def __init__(self):
self._friends = {} # {user_id: set(friend_ids)}
def add_friendship(self, user_a, user_b):
"""建立双向好友关系"""
self._friends.setdefault(user_a, set()).add(user_b)
self._friends.setdefault(user_b, set()).add(user_a)
def get_friends(self, user):
"""获取用户的好友集合"""
return self._friends.get(user, set())
def get_mutual_friends(self, user_a, user_b):
"""获取两个用户的共同好友"""
return self.get_friends(user_a) & self.get_friends(user_b)
def recommend(self, user, top_n=5):
"""
基于共同好友推荐新朋友。
排除已是好友的人和用户自己。
"""
my_friends = self.get_friends(user)
recommendations = {} # {candidate_id: mutual_friend_count}
# 遍历我每个好友的好友
for friend in my_friends:
for friend_of_friend in self.get_friends(friend):
# 排除自己和已是好友的人
if friend_of_friend != user and friend_of_friend not in my_friends:
# 计算共同好友数
mutual = len(self.get_mutual_friends(user, friend_of_friend))
recommendations[friend_of_friend] = mutual
# 按共同好友数排序
sorted_recs = sorted(recommendations.items(), key=lambda x: x[1], reverse=True)
return sorted_recs[:top_n]
def suggest_friends_of_friends(self, user):
"""
推荐"好友的好友"(二级关系)。
用集合运算实现。
"""
my_friends = self.get_friends(user)
# 所有好友的好友(二级关系)
friends_of_friends = set()
for friend in my_friends:
friends_of_friends |= self.get_friends(friend)
# 排除已经是好友的人
suggestions = friends_of_friends - my_friends - {user}
return suggestions
# 构建好友网络
fr = FriendRecommender()
edges = [
('Alice', 'Bob'), ('Alice', 'Charlie'), ('Alice', 'David'),
('Bob', 'Charlie'), ('Bob', 'Eve'),
('Charlie', 'David'), ('Charlie', 'Eve'), ('Charlie', 'Frank'),
('David', 'Frank'), ('David', 'Grace'),
('Eve', 'Frank'), ('Eve', 'Grace'),
]
for a, b in edges:
fr.add_friendship(a, b)
print('=== 好友推荐系统 ===\n')
for user in ['Alice', 'Bob', 'Frank']:
print(f'{user}的好友: {fr.get_friends(user)}')
print(f'\nAlice和Bob的共同好友: {fr.get_mutual_friends("Alice", "Bob")}')
print(f'Alice和Eve的共同好友: {fr.get_mutual_friends("Alice", "Eve")}')
print(f'\n给Alice的推荐:')
for candidate, mutual in fr.recommend('Alice'):
print(f' 推荐 {candidate}(共同好友: {mutual}人)')
print(f'\nAlice的好友的好友: {fr.suggest_friends_of_friends("Alice")}')
8.2 实战二:标签系统
# 场景:内容管理系统的标签匹配引擎
class TagMatcher:
"""基于集合运算的标签匹配引擎"""
def __init__(self):
self._articles = {} # {article_id: set(tags)}
self._tag_index = {} # {tag: set(article_ids)}——倒排索引
def add_article(self, article_id, tags):
"""添加文章及其标签"""
tag_set = set(tags)
self._articles[article_id] = tag_set
# 更新倒排索引
for tag in tag_set:
self._tag_index.setdefault(tag, set()).add(article_id)
def search_any(self, tags):
"""
搜索包含任意一个标签的文章(并集)。
适用场景:宽松搜索,扩大结果范围。
"""
search_tags = set(tags)
result_ids = set()
for tag in search_tags:
result_ids |= self._tag_index.get(tag, set())
return result_ids
def search_all(self, tags):
"""
搜索包含所有标签的文章(交集)。
适用场景:精确搜索,缩小结果范围。
"""
search_tags = set(tags)
if not search_tags:
return set()
# 从第一个标签的结果开始
result_ids = self._tag_index.get(list(search_tags)[0], set()).copy()
# 与其他标签取交集
for tag in list(search_tags)[1:]:
result_ids &= self._tag_index.get(tag, set())
if not result_ids: # 提前终止——交集为空
break
return result_ids
def search_exact(self, tags):
"""
精确匹配——文章标签必须完全等于搜索标签。
用对称差判断——对称差为空意味着完全相同。
"""
search_tags = set(tags)
result = set()
for article_id, article_tags in self._articles.items():
if article_tags == search_tags:
result.add(article_id)
return result
def search_excluding(self, include_tags, exclude_tags):
"""
包含某些标签但同时排除某些标签(差集)。
"""
included = self.search_all(include_tags)
excluded = self.search_any(exclude_tags)
return included - excluded
def get_related_articles(self, article_id, max_results=5):
"""
查找相关文章。
用Jaccard相似度——两篇文章标签的交集/并集。
"""
if article_id not in self._articles:
return []
source_tags = self._articles[article_id]
similarities = []
for other_id, other_tags in self._articles.items():
if other_id == article_id:
continue
# Jaccard相似度 = |A ∩ B| / |A ∪ B|
intersection = len(source_tags & other_tags)
union = len(source_tags | other_tags)
similarity = intersection / union if union > 0 else 0
similarities.append((other_id, similarity))
# 按相似度降序排列
similarities.sort(key=lambda x: x[1], reverse=True)
return similarities[:max_results]
def tag_coverage_stats(self):
"""统计标签覆盖情况"""
total_articles = len(self._articles)
print(f'总文章数: {total_articles}')
print(f'总标签数: {len(self._tag_index)}')
# 每篇文章的标签数
tag_counts = [len(tags) for tags in self._articles.values()]
print(f'平均每篇标签数: {sum(tag_counts) / total_articles:.1f}')
# 最常用的标签
tag_usage = {tag: len(articles) for tag, articles in self._tag_index.items()}
top_tags = sorted(tag_usage.items(), key=lambda x: x[1], reverse=True)[:5]
print('最常用标签:')
for tag, count in top_tags:
print(f' {tag}: {count}篇文章')
# 使用示例
matcher = TagMatcher()
# 添加文章
articles = [
(1, ['Python', '编程', '入门']),
(2, ['Python', 'Web', 'Django']),
(3, ['Python', '数据', 'Pandas']),
(4, ['Java', '编程', '入门']),
(5, ['Python', '编程', 'Web', 'Flask']),
(6, ['数据', '机器学习', 'Python']),
(7, ['Web', 'JavaScript', 'React']),
(8, ['Python', '编程', '算法']),
]
for aid, tags in articles:
matcher.add_article(aid, tags)
print('=== 标签匹配系统 ===\n')
# 搜索
print('搜索任意标签[Python, Web]:', matcher.search_any(['Python', 'Web']))
print('搜索所有标签[Python, Web]:', matcher.search_all(['Python', 'Web']))
print('搜索[Python, 编程]排除[Web]:', matcher.search_excluding(['Python', '编程'], ['Web']))
# 相关文章
print(f'\n文章1的相关文章:')
for aid, sim in matcher.get_related_articles(1):
print(f' 文章{aid}: 相似度{sim:.2%}')
# 标签统计
print(f'\n标签覆盖情况:')
matcher.tag_coverage_stats()
8.3 实战三:A/B测试用户分组
# 场景:为A/B测试分配用户组
import random
class ABTestManager:
"""A/B测试的用户分组管理器——集合运算的经典应用"""
def __init__(self, all_users):
self.all_users = set(all_users)
self.group_a = set()
self.group_b = set()
self.holdout = set() # 保留组(不参与测试)
def assign_groups(self, a_ratio=0.45, b_ratio=0.45, holdout_ratio=0.1):
"""
随机分配用户到A组、B组和保留组。
确保三组互不相交(用集合差集保证)。
"""
users = list(self.all_users)
random.shuffle(users)
n = len(users)
n_a = int(n * a_ratio)
n_b = int(n * b_ratio)
self.group_a = set(users[:n_a])
self.group_b = set(users[n_a:n_a + n_b])
self.holdout = set(users[n_a + n_b:])
# 验证:三组互不相交
assert self.group_a & self.group_b == set(), "A和B有交集!"
assert self.group_a & self.holdout == set(), "A和保留组有交集!"
assert self.group_b & self.holdout == set(), "B和保留组有交集!"
# 验证:三组并集 = 总用户
assert self.group_a | self.group_b | self.holdout == self.all_users, "有用户丢失!"
return {
'A组': len(self.group_a),
'B组': len(self.group_b),
'保留组': len(self.holdout),
}
def get_group(self, user):
"""查询用户属于哪个组"""
if user in self.group_a:
return 'A'
elif user in self.group_b:
return 'B'
elif user in self.holdout:
return 'HOLDOUT'
else:
return None
def add_new_users(self, new_users, assign_to='holdout'):
"""添加新用户——确保不与已有组重复"""
new_set = set(new_users) - self.all_users # 真正的新用户
if assign_to == 'holdout':
self.holdout |= new_set
elif assign_to == 'A':
self.group_a |= new_set
elif assign_to == 'B':
self.group_b |= new_set
self.all_users |= new_set
return len(new_set)
def get_overlap_report(self):
"""查看各组重叠情况(应该全部为空)"""
return {
'A∩B': self.group_a & self.group_b,
'A∩Holdout': self.group_a & self.holdout,
'B∩Holdout': self.group_b & self.holdout,
'未被分配的用户': self.all_users - self.group_a - self.group_b - self.holdout,
}
# 使用示例
users = [f'user_{i:03d}' for i in range(100)]
ab_test = ABTestManager(users)
distribution = ab_test.assign_groups(a_ratio=0.4, b_ratio=0.4, holdout_ratio=0.2)
print('=== A/B测试分组 ===')
print(f'分组结果: {distribution}')
print(f'重叠检查: {ab_test.get_overlap_report()}')
# 查询
print(f"\nuser_000 在组: {ab_test.get_group('user_000')}")
print(f"user_050 在组: {ab_test.get_group('user_050')}")
# 添加新用户
new = [f'new_user_{i}' for i in range(10)]
added = ab_test.add_new_users(new)
print(f'\n添加了{added}个新用户到保留组')
九、集合运算的图示理解
# 用文氏图(Venn Diagram)逻辑理解集合运算
#
# ┌───────────┐ ┌───────────┐
# │ a │ │ b │
# │ {1,2,3} │ │ {3,4,5} │
# │ │ │ │
# │ 1 2 │ │ 4 5 │
# │ ┌────┼──┼─┐ │
# │ │ 3 │ │ │ │
# │ └────┼──┼─┘ │
# │ │ │ │
# └───────────┘ └───────────┘
#
# a | b = {1, 2, 3, 4, 5} 并集——橙色+重叠+蓝色
# a & b = {3} 交集——重叠部分
# a - b = {1, 2} 差集——只在橙色部分
# b - a = {4, 5} 差集——只在蓝色部分
# a ^ b = {1, 2, 4, 5} 对称差——橙色+蓝色(不含重叠)
# 代码验证
a = {1, 2, 3}
b = {3, 4, 5}
print(f'a | b = {a | b}') # {1, 2, 3, 4, 5}
print(f'a & b = {a & b}') # {3}
print(f'a - b = {a - b}') # {1, 2}
print(f'b - a = {b - a}') # {4, 5}
print(f'a ^ b = {a ^ b}') # {1, 2, 4, 5}
十、常见陷阱和注意事项
10.1 陷阱一:运算符两边类型必须一致
a = {1, 2, 3}
# ❌ 集合运算符不能和列表/元组混用
# a | [4, 5] # TypeError
# a & (3, 4) # TypeError
# a - [1, 2] # TypeError
# a ^ (3, 4) # TypeError
# ✅ 先用set()转换
result = a | set([4, 5])
print(result) # {1, 2, 3, 4, 5}
# ✅ 或者用方法(接受可迭代对象)
result = a.union([4, 5])
print(result) # {1, 2, 3, 4, 5}
10.2 陷阱二:运算符的优先级
# 集合运算符的优先级(从高到低):
# -(差集)
# &(交集)
# ^(对称差集)
# |(并集)
# 没有括号时,按优先级计算
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
c = {1, 5, 7}
# a - b & c # 这等于什么?
# & 优先级高于 -,所以先算 b & c = {5}
# 然后算 a - {5} = {1, 2, 3, 4}
result = a - b & c
print(f'a - b & c = {result}') # {1, 2, 3, 4}
# 加括号更清晰
result = a - (b & c)
print(f'a - (b & c) = {result}') # {1, 2, 3, 4}
# ⚠️ 建议:不确定优先级时,用括号明确意图
# 即使你知道优先级,加括号也能让代码更易读
10.3 陷阱三:空集合的处理
# 空集合的运算结果取决于运算类型
a = {1, 2, 3}
empty = set()
# 并集——返回非空集合
print(a | empty) # {1, 2, 3}
print(empty | a) # {1, 2, 3}
# 交集——返回空集合
print(a & empty) # set()
print(empty & a) # set()
# 差集——返回非空集合
print(a - empty) # {1, 2, 3}
print(empty - a) # set()
# 对称差——返回非空集合
print(a ^ empty) # {1, 2, 3}
print(empty ^ a) # {1, 2, 3}
10.4 陷阱四:可变元素会导致运算失败
# ❌ 包含列表的"集合"根本创建不了
# s = {1, 2, [3, 4]} # TypeError: unhashable type: 'list'
# 但如果集合中包含可变元素(通过其他方式),运算会报错
# 实际上Python会阻止这种情况,所以你不需要太担心
# 但需要注意:元组中包含列表也不能放入集合
# s = {(1, [2, 3]), (4, 5)} # TypeError: unhashable type: 'list'
十一、集合运算的组合模式
# 在实际项目中,集合运算经常以组合模式出现
# 模式一:包含但不完全等于(差集+交集)
def has_overlap_but_not_equal(a, b):
"""a和b有交集但互不包含"""
return bool(a & b) and not (a <= b) and not (b <= a)
# 模式二:Jaccard相似度
def jaccard_similarity(a, b):
"""衡量两个集合的相似度"""
intersection = len(a & b)
union = len(a | b)
return intersection / union if union > 0 else 0
# 模式三:包含度(Overlap Coefficient)
def overlap_coefficient(a, b):
"""b中有多少比例的元素也在a中"""
intersection = len(a & b)
return intersection / min(len(a), len(b)) if min(len(a), len(b)) > 0 else 0
# 模式四:集合相等(考虑容忍度)
def almost_equal(a, b, threshold=0.9):
"""两个集合的Jaccard相似度超过阈值就认为相等"""
return jaccard_similarity(a, b) >= threshold
# 使用示例
set1 = {'Python', 'Java', 'JavaScript', 'Go'}
set2 = {'Python', 'JavaScript', 'Rust', 'TypeScript'}
set3 = {'C++', 'C#', 'Ruby', 'Swift'}
print(f'Jaccard({set1}, {set2}) = {jaccard_similarity(set1, set2):.2%}')
print(f'Jaccard({set1}, {set3}) = {jaccard_similarity(set1, set3):.2%}')
print(f'has_overlap({set1}, {set2}): {has_overlap_but_not_equal(set1, set2)}')
print(f'has_overlap({set1}, {set3}): {has_overlap_but_not_equal(set1, set3)}')
十二、本篇小结
集合的四种核心运算,每一种都有运算符和方法两种写法:
并集(Union)—— | / union():
- 返回两(多)个集合中所有元素(自动去重)
- union() 可接受任意可迭代对象,可同时传多个参数
- 经典场景:合并多数据源、汇总去重
交集(Intersection)—— & / intersection():
- 返回两(多)个集合的共有元素
- 时间复杂度 O(min(len(a), len(b)))
- 经典场景:找共同特征、多条件筛选、共同好友
差集(Difference)—— - / difference():
- 返回在前面集合但不在后面集合中的元素
- ⚠️ 不对称:a - b ≠ b - a
- 经典场景:数据增量分析(新增/删除)、过滤排除、权限限制
对称差(Symmetric Difference)—— ^ / symmetric_difference():
- 返回只在其中一个集合中的元素(排除共有元素)
- ⚠️ 对称:a ^ b = b ^ a
- ⚠️ 方法只接受一个参数
- 经典场景:找差异、版本变更、A/B组的差异分析
学完集合的创建、去重和四大运算,你已经掌握了Python集合的全部核心能力。实际开发中,这些运算在数据分析、推荐系统、权限管理、标签匹配等场景中使用频率极高。
以上就是Python基础指南之集合的四种基本运算详解的详细内容,更多关于Python集合运算的资料请关注脚本之家其它相关文章!
