2.10. 集合

集合(set)是由唯一元素组成的无序集合。添加一个已存在的值不会产生任何效果;迭代时每个值恰好产生一次。当成员判断和去重很重要、而顺序无关紧要时,集合就是合适的工具。

2.10.1. 创建集合

对于非空集合使用花括号,对于空集合使用 set()

colours = {"red", "green", "blue"}
empty = set()

花括号看起来像 dict 字面量;单独的 {} 是一个空字典,而不是空集合——这是 Python 的历史遗留问题之一。空集合的情况请使用 set()

set() 也能从任意可迭代对象构建集合,这是从序列中去除重复项的标准方式:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

输出:

{1, 2, 3, 4}

打印顺序可能不同——集合不保证以任何特定顺序迭代。

2.10.2. 集合与字典对比

集合和字典都在哈希表中存储唯一元素。区别在于每个元素随身携带的内容:

  • dict 存储键值对。查找一个键会返回它的值。

  • set 只存储元素本身。查找一个元素会告诉你它是否存在。

两者之间的选择,取决于每个元素旁边的值是否有意义:

  • 当没有值需要伴随每个元素时,选用集合——你只关心元素是否存在,或者你正用并集 / 交集来组合若干组唯一元素。

  • 当每个元素都与某些数据配对、且查找的目的就是取回这些数据时,选用字典——配置映射、缓存、按名字作键的计数器。

这两种类型在表面语法上有很多相似之处,这正是大多数混淆的来源。下面用一段来对比它们的区别:

set

dict

存放

唯一元素

唯一键,每个键带一个值

非空字面量

{1, 2, 3}

{"a": 1, "b": 2}

空字面量

set()

{}

成员测试

x in s

k in d(仅键)

取一个值

不适用

d[k]

添加一个元素

s.add(x)

d[k] = v

迭代

产生元素

产生键(用 d.items() 取键值对)

非空字面量与空字面量之间的不对称性是值得特别指出的陷阱:

  • 里面带元素的花括号——{1, 2, 3}——是集合字面量;带键值对的花括号——{"a": 1}——是字典字面量。解析器靠里面的内容来区分它们。

  • 里面什么都没有的花括号——{}——是空字典,而不是空集合。字典出现得更早;空字面量归它所有。空集合根本没有花括号字面量,必须写成 set()

当一个字典的值从不被读取、只读取它的键时,一种常见的模式是改用集合——这能让意图一目了然,并把未使用的值从内存中削减掉。

2.10.3. 添加和删除

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

输出:

{1, 3, 4}

2.10.4. 成员判断

in 运算符测试成员关系。在集合上,无论大小它都大致是常数时间——这是当你只需要询问"这个值是否在里面"时,选择集合而非 list 的主要原因:

if "red" in colours:
    print("colour is allowed")

内容相同的 list 每次都会从头开始扫描,这对十个元素没问题,但对一万个元素就很慢了。

2.10.5. 集合运算

两个集合可以用常见的数学运算来组合。每种运算都同时具有运算符形式和方法形式:

  • a | ba.union(b) —— 任一集合中的所有元素。

  • a & ba.intersection(b) —— 只有同时出现在两者中的元素。

  • a - ba.difference(b) —— 在 a 中但不在 b 中的元素。

  • a ^ ba.symmetric_difference(b) —— 在其中一个但不在两者中的元素。

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

输出:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

运算符形式是只读的;方法形式接受右侧的任意可迭代对象,而不仅仅是另一个集合(a.union([5, 6]))。选用在上下文中读起来更顺的那种。

2.10.6. 什么可以放进集合

集合元素必须是可哈希的——与 dict 键的约束相同。intfloatstrboolbytes 以及 tuple(当其内容本身都可哈希时)都可以。listdict 不行;尝试添加其中之一会引发 TypeError

2.10.7. frozenset

普通的 set 是可变的:每次调用 add / remove / discard 都会就地改变该对象。这种可变性使它失去可哈希的资格,因此集合不能用作 dict 的键,也不能作为另一个集合的成员。

frozenset 是其不可变的对应物。它具有与 set 相同的查找和运算符(in|&-^),但没有 add / remove 等任何会修改它的方法。由于其内容永远无法改变,frozenset 的哈希值是良定义的——因此它可哈希的:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

输出:

RGB

从任意可迭代对象构造 frozenset——frozenset() 用于空的情况,frozenset(some_set) 用于对现有集合拍一份不可变的快照:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

选用它的两个常见理由:

  • 用作字典键或集合成员。 任何单个值无法表达你所需内容的地方,一个由若干值构成的 frozenset 都可以——"这个驱动程序支持的特性集合"、"这个配置使用的引脚集合"。

  • 锁定一个常量。 模块级的允许名称 frozenset 不会被调用方意外修改;普通的 set 则会。对于任何意图在构造之后只读的内容,优先使用 frozenset