Python基础指南之集合的四种基本运算详解

 更新时间:2026年06月15日 08:40:02   作者:星河耀银海  
本文详细介绍了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 - bb - 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 sO(1)哈希表查找
a | bO(len(a) + len(b))遍历两个集合
a & bO(min(len(a), len(b)))遍历较小的集合
a - bO(len(a))遍历a,检查是否在b中
a ^ bO(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集合运算的资料请关注脚本之家其它相关文章!

相关文章

最新评论