十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python集合(Set)完全指南:从去重到关系运算的实战技巧

Python集合(Set)完全指南:从去重到关系运算的实战技巧 1. 集合Set在Python中的核心定位与价值如果你是从其他编程语言比如Java转到Python或者刚开始系统学习Python的数据结构那么set集合绝对是一个会让你感到惊喜的存在。它不像列表list或元组tuple那样强调顺序也不像字典dict那样存储键值对。集合的核心就两点唯一性和无序性。简单说它就是一个装着一堆不重复元素的“袋子”你没法保证从里面拿出元素的顺序但你能确信里面的每个东西都是独一无二的。在实际开发中这个特性解决了一类非常高频且棘手的问题去重和关系测试。想象一下你从数据库拉取用户ID列表或者从日志文件中解析IP地址里面难免有重复项。用列表处理你得写循环用in判断效率低下。而用集合一行代码set(my_list)就能搞定背后的哈希表实现让它的成员检测时间复杂度接近O(1)速度快得飞起。再比如你有两组数据需要快速找出它们的共同项交集、不同项差集或者合并项并集集合提供的原生运算符,-,|就像是为这种场景量身定做的代码简洁意图明确性能还高。很多新手甚至一些有经验的开发者在处理这类问题时第一反应还是用列表和循环去硬怼写出来的代码又长又慢。其实把集合这个工具用好了代码的简洁性和效率都能提升一个档次。它不是什么高深莫测的黑科技而是Python内置的、非常接地气的实用工具。接下来我们就把它掰开揉碎了从创建到操作从原理到坑点彻底讲清楚。2. 集合的创建与基础特性解析2.1 两种创建方式与空集合陷阱创建集合主要有两种方式各有其适用场景。第一种使用花括号{}。这是最直观、最常用的方式特别适合你知道集合初始元素的时候。# 创建一个包含若干元素的集合 fruits {apple, banana, orange, apple} # 重复的apple会被自动去重 print(fruits) # 输出可能是{banana, orange, apple} 无序这里有个关键点虽然用了花括号但它和字典dict完全不同。字典里是{key: value}键值对而集合里直接放单个元素。Python解释器会根据上下文来区分。第二种使用set()构造函数。这个方式更灵活它可以将任何可迭代对象iterable转换为集合。# 从列表创建 list_data [1, 2, 2, 3, 4] set_from_list set(list_data) print(set_from_list) # 输出{1, 2, 3, 4} # 从字符串创建每个字符会成为独立元素 set_from_str set(hello) print(set_from_str) # 输出{e, h, l, o} 注意只有一个l # 从元组、字典键等创建 set_from_tuple set((5, 6, 7)) set_from_dict_keys set({a: 1, b: 2}) # 得到 {a, b}这里有一个经典的“坑”关于空集合的创建。# 错误示范这样创建的是一个空字典不是空集合 empty_dict {} print(type(empty_dict)) # 输出class dict # 正确示范必须使用set()函数 empty_set set() print(type(empty_set)) # 输出class set这个陷阱我踩过不止一次特别是在函数里动态初始化一个集合时顺手写了{}结果后面进行集合操作时报错调试半天才发现类型不对。记住空集合有且只有set()这一种创建方式。2.2 核心特性无序、唯一与元素要求创建好集合后它的几个核心特性决定了我们如何使用它。1. 无序性Unordered集合不记录元素的插入顺序。当你打印一个集合或遍历它时元素的出现顺序是不可预测的它取决于内部哈希表的状态。这意味着你不能通过索引如my_set[0]来访问集合元素这会引发TypeError。两次遍历同一个集合元素的顺序可能不同尽管在单次Python运行会话中对于不变集合顺序通常是稳定的但你不应依赖这一点。2. 唯一性Unique这是集合的立身之本。所有重复的元素在创建时会被自动剔除。这个特性基于元素的哈希值Hash和相等性比较Equality。3. 元素必须是“可哈希的”Hashable这是理解集合和字典键的底层关键。一个对象是可哈希的意味着在其生命周期内它的哈希值永不改变需要实现__hash__()方法并且可以与其他对象进行比较需要实现__eq__()方法。可哈希的常见类型整数int、浮点数float、字符串str、元组tuple且其所有元素也必须可哈希、冻结集合frozenset。不可哈希的常见类型列表list、字典dict、集合set本身。为什么有这个限制因为集合内部使用哈希表来实现高速查找。哈希表根据元素的哈希值决定其存储位置。如果一个元素的哈希值会变比如列表可以增删元素那么它被放入集合后如果内容改变哈希值也变集合就再也找不到它了这会导致数据混乱和内存泄漏。# 可哈希元素可以放入集合 valid_set {1, 3.14, hello, (1, 2)} # 不可哈希元素尝试放入会报错 invalid_set {[1, 2]} # TypeError: unhashable type: list invalid_set2 {{a: 1}} # TypeError: unhashable type: dict注意这里有一个进阶理解点。我们说集合元素必须“可哈希”但如果你放入一个自定义的类对象呢默认情况下用户定义的类实例是可哈希的其哈希值基于对象id。但如果你重写了类的__eq__方法就必须同时重写__hash__方法并确保相等的对象具有相同的哈希值否则这个类的实例在作为集合元素或字典键时行为会不符合预期甚至破坏数据结构。这是一个高级主题但在设计需要放入集合的类时至关重要。3. 集合的常用操作增删改查掌握了集合的创建和特性我们来看看日常最频繁的操作。这些操作大多都有两种形式一种是通过方法method另一种是通过运算符operator或内置函数。方法调用更清晰适合流程控制运算符形式更简洁适合在表达式中使用。3.1 增加元素向集合中添加新元素主要用add()和update()方法。add(elem)添加单个元素。如果元素已存在则集合不变。s {1, 2, 3} s.add(4) print(s) # {1, 2, 3, 4} s.add(2) # 添加已存在的元素无效果 print(s) # 仍然是 {1, 2, 3, 4}update(*others)批量添加元素。参数可以是多个可迭代对象列表、元组、集合、字符串等它会将每个可迭代对象中的元素逐个添加到原集合中。s {1, 2} s.update([3, 4], (5,), {6, 7}, ab) # 合并列表、元组、集合、字符串 print(s) # 输出可能是{1, 2, 3, 4, 5, 6, 7, a, b}update()是原地修改没有返回值返回None。它非常高效比用循环多次调用add()要好。3.2 删除元素删除操作需要小心因为集合无序你无法指定删除“第几个”元素。常用的删除方法有remove()、discard()、pop()和clear()。remove(elem)移除指定元素。如果元素不存在会引发KeyError。s {1, 2, 3} s.remove(2) print(s) # {1, 3} # s.remove(4) # 这会引发 KeyError: 4当你确信元素存在时用remove()。discard(elem)移除指定元素。与remove()的关键区别在于如果元素不存在它不会报错而是静默地什么都不做。s {1, 2, 3} s.discard(2) print(s) # {1, 3} s.discard(4) # 元素不存在但不会报错 print(s) # 仍然是 {1, 3}在不确定元素是否存在又不想处理异常的场景下discard()是更安全的选择。这也是我个人的首选除非业务逻辑要求元素必须存在。pop()随机移除并返回集合中的一个元素。因为集合无序所以“弹出”哪个元素是不确定的。如果集合为空会引发KeyError。s {apple, banana, orange} item s.pop() print(item) # 可能是三个水果中的任意一个 print(s) # 剩下的两个水果pop()的一个常见用途是获取集合中的任意一个元素进行处理同时将其从集合中移除。比如在实现某些算法如随机游走、任务分发时。clear()清空集合移除所有元素。s {1, 2, 3} s.clear() print(s) # set()3.3 查询与判断集合的查询操作主要是判断元素是否存在以及获取集合的基本信息。in和not in运算符这是最常用的成员检测基于哈希表速度极快平均O(1)。s {1, 2, 3} print(2 in s) # True print(5 not in s) # Truelen(s)返回集合中元素的数量。s {1, 2, 2, 3} print(len(s)) # 3重复的2只算一个3.4 遍历集合由于集合无序遍历时没有固定顺序。通常使用for循环。fruits {apple, banana, orange} for fruit in fruits: print(fruit) # 输出顺序不定可能是 banana, orange, apple如果你需要按特定顺序处理集合元素一个常见的做法是先将其转换为列表然后排序。sorted_fruits sorted(fruits) # 返回一个排序后的列表 print(sorted_fruits) # [apple, banana, orange]sorted()函数总是返回一个新列表不会修改原集合。4. 集合的关系运算并、交、差、对称差这是集合数据类型最闪耀的地方它能用极其简洁的语法表达复杂的集合关系。这些运算都有两种形式方法形式和运算符形式。我通常这样记需要将结果赋给新变量时两种都可以需要原地修改原集合时用带update前缀的方法或特定运算符。4.1 并集Union包含所有出现在两个集合中的元素。方法union(*others)返回新集合。运算符|(管道符)。原地更新方法update(*others)(前面讲过它相当于并集的原地操作)。A {1, 2, 3} B {3, 4, 5} # 方法形式 C A.union(B) print(C) # {1, 2, 3, 4, 5} # 运算符形式 C A | B print(C) # {1, 2, 3, 4, 5} # 原地修改A相当于 A | B A.update(B) print(A) # {1, 2, 3, 4, 5}4.2 交集Intersection只包含同时出现在两个集合中的元素。方法intersection(*others)返回新集合。运算符。原地更新方法intersection_update(*others)。A {1, 2, 3, 4} B {3, 4, 5, 6} # 方法形式 C A.intersection(B) print(C) # {3, 4} # 运算符形式 C A B print(C) # {3, 4} # 原地修改A相当于 A B A.intersection_update(B) print(A) # {3, 4}4.3 差集Difference包含在第一个集合中但不在第二个集合中的元素。注意顺序A - B不等于B - A。方法difference(*others)返回新集合。运算符-(减号)。原地更新方法difference_update(*others)。A {1, 2, 3, 4} B {3, 4, 5} # A 有而 B 没有的 C A.difference(B) # 或 C A - B print(C) # {1, 2} # B 有而 A 没有的 D B.difference(A) # 或 D B - A print(D) # {5} # 原地修改A移除A中也在B里的元素相当于 A - B A.difference_update(B) print(A) # {1, 2}4.4 对称差集Symmetric Difference包含只出现在两个集合之一的元素即并集减去交集。它等价于(A - B) | (B - A)。方法symmetric_difference(other)返回新集合。运算符^(脱字符)。原地更新方法symmetric_difference_update(other)。A {1, 2, 3} B {3, 4, 5} # 方法形式 C A.symmetric_difference(B) print(C) # {1, 2, 4, 5} # 运算符形式 C A ^ B print(C) # {1, 2, 4, 5} # 原地修改A相当于 A ^ B A.symmetric_difference_update(B) print(A) # {1, 2, 4, 5}4.5 子集与超集判断用于判断两个集合之间的包含关系。子集Subsetissubset(other)或。判断当前集合的所有元素是否都在另一个集合中。真子集Proper Subset。是子集且两个集合不相等。超集Supersetissuperset(other)或。判断当前集合是否包含另一个集合的所有元素。真超集Proper Superset。是超集且两个集合不相等。A {1, 2} B {1, 2, 3} C {1, 2} print(A.issubset(B)) # True, A B print(A B) # True, A是B的真子集 print(A C) # True, A是C的子集 print(A C) # False, A等于C不是真子集 print(B.issuperset(A)) # True, B A print(B A) # True, B是A的真超集4.6 不相交判断isdisjoint(other)如果两个集合没有共同元素交集为空则返回True。这在检查两组数据是否完全独立时非常有用。set1 {1, 2, 3} set2 {4, 5, 6} set3 {3, 4} print(set1.isdisjoint(set2)) # True print(set1.isdisjoint(set3)) # False因为有共同元素35. 集合推导式与冻结集合5.1 集合推导式Set Comprehension和列表推导式类似集合推导式提供了一种简洁、高效创建集合的方法。语法是{expression for item in iterable if condition}。# 创建一个包含1到10之间偶数的集合 even_squares {x**2 for x in range(1, 11) if x % 2 0} print(even_squares) # {64, 4, 36, 100, 16} # 注意输出是无序的元素是 4, 16, 36, 64, 100 # 从字符串列表中提取长度大于3的单词并转换为小写 words [Apple, banana, CHERRY, date, Elderberry] long_words {word.lower() for word in words if len(word) 3} print(long_words) # 输出可能是{cherry, banana, elderberry, apple}集合推导式在去重和转换一步完成时特别方便。它背后的执行过程是先构建一个临时列表或类似结构然后传递给set()构造函数但由于是语法糖其效率通常很高。5.2 冻结集合frozensetfrozenset是集合的不可变版本。一旦创建就不能增删改其中的元素。正因为它是不可变的所以它是可哈希的可以作为字典的键或另一个集合的元素。# 创建冻结集合 fs frozenset([1, 2, 3, 3]) print(fs) # frozenset({1, 2, 3}) print(type(fs)) # class frozenset # 尝试修改会报错 # fs.add(4) # AttributeError: frozenset object has no attribute add # fs.remove(1) # 同样报错 # 冻结集合可以作为字典的键 dict_with_fs_key {fs: 这是一个冻结集合} print(dict_with_fs_key) # {frozenset({1, 2, 3}): 这是一个冻结集合} # 冻结集合也可以作为另一个集合的元素 nested_set {fs, frozenset([4,5])} print(nested_set) # {frozenset({4, 5}), frozenset({1, 2, 3})}frozenset支持所有不修改集合本身的操作如len(),in,for循环以及所有的关系运算union,intersection等这些操作会返回一个新的frozenset或普通set。当你需要一组不可变的、可作为哈希键的数据时frozenset就派上用场了。6. 集合的典型应用场景与实战技巧理解了基本操作我们来看看集合在真实项目中如何大显身手。很多场景下用集合思考代码会变得异常简洁。6.1 场景一数据去重最经典用途这是集合的“杀手级”应用。任何需要从序列中剔除重复项的场景set()都是首选。# 场景从日志文件中提取唯一的IP地址 log_entries [192.168.1.1 - GET /, 10.0.0.2 - POST /login, 192.168.1.1 - GET /home] # 假设我们简单分割取第一个字段作为IP ip_list [entry.split()[0] for entry in log_entries] unique_ips set(ip_list) print(unique_ips) # {192.168.1.1, 10.0.0.2} # 更复杂的去重基于对象的某个属性去重 class User: def __init__(self, id, name): self.id id self.name name def __repr__(self): return fUser({self.id}, {self.name}) users [User(1, Alice), User(2, Bob), User(1, Alice), User(3, Charlie)] # 直接对对象列表用set()不行因为对象默认哈希基于id两个id为1的Alice对象id不同。 # 需要先转换为可哈希的元组基于id或使用其他技巧。 unique_users_by_id {user.id: user for user in users}.values() # 利用字典键唯一性 print(list(unique_users_by_id)) # [User(1, Alice), User(2, Bob), User(3, Charlie)]注意set()去重会丢失原始顺序。如果你需要保留元素首次出现的顺序可以使用dict.fromkeys()这个技巧Python 3.7 字典保序my_list [b, a, c, a, b] unique_ordered list(dict.fromkeys(my_list)) print(unique_ordered) # [b, a, c]6.2 场景二成员关系快速测试当需要频繁检查某个元素是否存在于一个大型集合中时集合的in操作O(1)平均复杂度远快于列表O(n)。# 构建一个大的有效词表 valid_words set(open(large_word_list.txt).read().splitlines()) # 假设有十万个词 # 快速检查用户输入是否有效 user_input python if user_input in valid_words: print(单词有效) else: print(单词不在词表中。)这种场景在缓存、权限校验、过滤等系统中非常常见。6.3 场景三集合运算处理数据关系这是体现集合思维优势的地方。假设你有两个用户群newsletter_subscribers订阅新闻和premium_members付费会员。newsletter_subscribers {aliceexample.com, bobexample.com, charlieexample.com} premium_members {bobexample.com, davidexample.com} # 找出订阅了新闻但不是付费会员的人可能想给他们发升级广告 subscribers_not_premium newsletter_subscribers - premium_members print(subscribers_not_premium) # {aliceexample.com, charlieexample.com} # 找出既是订阅者又是付费会员的人核心用户 loyal_users newsletter_subscribers premium_members print(loyal_users) # {bobexample.com} # 找出所有联系过的用户并集 all_contacts newsletter_subscribers | premium_members print(all_contacts) # {aliceexample.com, bobexample.com, charlieexample.com, davidexample.com} # 找出只在其中一个群组中的人对称差 exclusive_members newsletter_subscribers ^ premium_members print(exclusive_members) # {aliceexample.com, charlieexample.com, davidexample.com}用几行清晰的集合运算就完成了可能需要多重循环和条件判断才能搞定的逻辑。6.4 场景四实现类似“已处理”或“已访问”的记录在图遍历、网页爬虫、任务调度中经常需要记录已经访问过的节点避免重复处理。集合是绝佳的选择。visited_urls set() url_queue [https://example.com/page1] while url_queue: current_url url_queue.pop(0) if current_url in visited_urls: continue # 跳过已访问的 visited_urls.add(current_url) # ... 处理当前URL并从中提取新的链接添加到url_queue ...集合的in和add操作都非常快使得这种检查高效且易于实现。7. 性能考量、常见陷阱与最佳实践7.1 性能特点成员检测 (in,not in)、添加 (add)、删除 (discard,remove)平均时间复杂度为O(1)。这是哈希表数据结构带来的红利也是集合最核心的优势。并、交、差等关系运算时间复杂度一般为O(len(s) len(t))在最坏情况下可能更高但通常比用循环手动实现快得多。与列表对比对于in操作列表是O(n)集合是O(1)。当数据量很大比如超过几百个元素且需要频繁查找时集合的性能优势是指数级的。一个简单的性能对比import time large_list list(range(1000000)) large_set set(large_list) # 测试在列表中查找最后一个元素最坏情况 start time.time() _ 999999 in large_list list_time time.time() - start # 测试在集合中查找同一个元素 start time.time() _ 999999 in large_set set_time time.time() - start print(fList in time: {list_time:.6f} seconds) print(fSet in time: {set_time:.6f} seconds) # 输出可能类似List in time: 0.010123 seconds, Set in time: 0.000003 seconds7.2 常见陷阱与避坑指南陷阱一误用空集合{}前面提过这是最常见的错误。牢记{}是空字典set()才是空集合。陷阱二试图存储不可哈希元素记住集合的元素必须是不可变的可哈希的。列表、字典、其他集合都不能直接作为集合元素。如果需要存储列表的集合可以考虑将列表转换为元组。list_of_lists [[1, 2], [3, 4], [1, 2]] # set_of_lists set(list_of_lists) # 报错 set_of_tuples {tuple(lst) for lst in list_of_lists} print(set_of_tuples) # {(1, 2), (3, 4)}陷阱三依赖集合元素的顺序永远不要假设集合遍历或打印的顺序。虽然从Python 3.7开始字典保持了插入顺序但集合仍然是无序的。在Python 3.6及之前甚至同一段代码两次运行集合的输出顺序都可能不同。如果需要顺序请使用sorted()。陷阱四在迭代过程中修改集合这是一个普遍适用于Python可变容器的规则。在遍历集合时不要直接对其增删元素这会导致运行时错误或不可预知的行为。s {1, 2, 3, 4, 5} # 错误做法在循环中删除元素 # for x in s: # if x % 2 0: # s.remove(x) # RuntimeError: Set changed size during iteration # 正确做法先收集要删除的元素或者使用集合推导式 to_remove {x for x in s if x % 2 0} s.difference_update(to_remove) print(s) # {1, 3, 5}陷阱五混淆remove()和discard()remove(elem)在元素不存在时会抛出KeyError而discard(elem)则不会。除非你确定元素一定存在或者希望用异常来提示“元素未找到”这个错误状态否则更推荐使用discard()代码更健壮。7.3 最佳实践总结明确需求选择数据结构当你的需求核心是“唯一性”和“快速存在性测试”时优先考虑集合。如果需要保持顺序或允许重复则用列表。善用集合运算简化逻辑遇到需要找共同点、差异点、合并数据时先想想能不能用集合的、-、|、^来表达这往往能让代码更声明式、更易读。大容量去重用set()这是将列表转为集合最直接的理由一行代码的事。注意哈希与不可变性自定义类对象作为集合元素或字典键时如果重写了__eq__务必同时正确重写__hash__。性能敏感处用集合在循环内频繁进行in判断时如果数据源是静态或变化不频繁的将其预先转换为集合能带来巨大的性能提升。使用frozenset作为键当你需要一个不可变的、可哈希的集合时frozenset是你的朋友特别是在需要将集合作为字典键的场景。集合是Python内置数据类型中一颗低调但璀璨的明珠。它用简单的规则唯一、无序和高效的实现哈希表解决了一大类实际问题。从简单的去重到复杂的关系运算再到高性能的成员检测掌握好集合能让你的Python代码更加简洁、优雅和高效。下次当你面对一堆需要处理的数据时不妨先问自己一句“这个问题用集合会不会更简单”
返回列表