Skip to content
This page has been auto-translated and may contain errors.View in English

元组与集合 ​

你已经了解了列表。Python 还有两种集合类型,用来解决列表解决不了的问题。元组保存一组固定不变、永远不会改变的值。集合只保存唯一值,无论集合有多大,都能瞬间判断某个值是否存在其中。

Python 的集合工具箱里有四种类型。列表和字典能应付大多数通用场景。元组和集合解决的是更具体的问题:不可变性有用的固定记录,以及以 O(1) 成员检测为首要目标的唯一值集合。

除了 list 和 dict,Python 还提供了 tuple(创建之后就不能再改变的固定长度序列)和 set(基于哈希表构建的唯一值无序集合,哈希表就是让 dict 查找飞快的那种按内容定位的结构)。下面所有决策都围绕这一区别展开:元组是可哈希的(它的内容可以归约成一个数字,因此能作为字典的键或集合的成员),而列表不是。该怎么选,要看你需要这个集合完成什么任务,而不是凭习惯随手选一个。

元组 ​

元组是一组创建之后就不能再改变的有序值。圆括号用来定义元组,但其实是可选的。真正让它成为元组的是逗号。只有一个元素的元组必须带一个结尾逗号。

元组是不可变序列。创建元组的是逗号,不是圆括号。当所有元素都是可哈希的,不可变性就让元组本身也变得可哈希,这就打开了列表做不到的用途:作为字典的键、集合的成员,以及固定结构的记录。

tuple 是一旦创建就不能再改变的固定长度序列。正因为其内部什么都不会变动,只要每个元素本身都可哈希,Python 就能为整个元组计算出唯一的哈希值(一个由其内容推导出的数字),这正是元组能作为字典键或集合成员而列表不能的原因。索引和切片的用法与列表完全一致;但不支持按项赋值,所以执行 point[0] = 99 这行代码时会抛出 TypeError。只有一个元素的写法 (42,) 需要那个结尾逗号,少了它,圆括号就只是起分组作用,得到的仍是裸值 42。

python
point = (10, 20)
rgb = (255, 128, 0)
dimensions = (1920, 1080)
single = (42,)            # 单元素元组必须有结尾逗号
also_tuple = 42, 99       # 圆括号是可选的;逗号才是元组的关键

按索引访问的方式和列表完全一样。尝试修改某一项会抛出 TypeError:

索引、切片和负数索引的用法都和列表完全相同。任何通过索引赋值的尝试都会抛出 TypeError;这是设计上的刻意行为,不是什么局限。

从元组读取数据,无论是按索引、负数索引还是切片,表现都和列表完全一致。真正不同的是写入:由于没有按项赋值,一旦执行 point[0] = 99 就会立刻抛出 TypeError。这正是选择元组而不是列表的全部意义所在——它是一个你和后来任何读代码的人都能放心信任、不会在背后被悄悄改动的值。

python
point = (10, 20)
point[0]    # 10
point[1]    # 20
point[-1]   # 20

point[0] = 99    # TypeError: 'tuple' object does not support item assignment
Juno元组 元组是一组创建之后就不能改变的有序值。真正创建它的是逗号,不是圆括号,这也是为什么 (42,) 需要那个孤零零的结尾逗号。想重新赋值某一项,你会得到一个 TypeError,而这种"锁死"的感觉正是你选择元组的理由。
Juno元组 元组是不可变序列,创建它的是逗号,不是圆括号。这种不可变性是一个特性:当内容都可哈希时,它让元组本身也可哈希,从而能作为字典键或集合成员,这是列表做不到的。按项赋值会按设计抛出 TypeError。
Juno元组 读取元组和列表一样,缺的是写入,所以 point[0] = 99 在运行时会抛出 TypeError。就靠这一点:当你需要保证没有任何下游代码会重写这个值时,元组就是你要传递的那个值。单元素的 (42,) 这个陷阱,每个人都会踩一次。

什么时候该用元组 ​

当你有一小组彼此相关、不会再变化的值时,就用元组。坐标 (x, y)、颜色 (r, g, b)、姓名和分数的搭配 ("Alice", 87)。这种固定结构会向阅读代码的人传递一个信号:这组数据要当作一个整体来对待。

元组传达的是固定结构:一组值,其中位置本身就承载着含义,整组数据被当作一个整体来对待。它的可哈希性使它可以作为字典的键,这是列表做不到的。元组传递的约定是:这些值本来就该在一起,而且不应该改变。

当你面对的是一条固定形状的记录时,就该用元组:字段数量固定,每个位置都有特定含义。因为元组可哈希(它的内容能归约成一个数字),所以它能用在任何需要可哈希值的地方:字典的键、集合的成员,或者 functools.lru_cache 背后被缓存的参数(它会记住一个函数在某组调用参数下的结果)。元组传递的语义信号也和列表不同。元组说的是"这些字段本来就该在一起,每个位置都有特定含义"(比如一个坐标、一种颜色),而列表说的是"一串相似的东西,长度可以变化"。

Juno什么时候该用元组 当你有一组固定的小数据、每个位置都有特定含义时,就用元组:一个坐标、一个 (r, g, b) 颜色、一对姓名和分数。因为元组可哈希,你甚至可以把它当作字典的键来用。列表做不到这一点,很多人第一次尝试时都会被这个坑到。
Juno什么时候该用元组 元组传递的是固定结构:位置本身就有含义,整组数据当作一个整体来使用。这就是它和列表的区别,列表说的是"一个长度可以变化的序列"。而且由于元组可哈希,它能作为字典的键,列表用在这里会抛出 TypeError。
Juno什么时候该用元组 固定形状的记录用元组,长度可变的一串相似数据用列表。真正的好处在于可哈希性:元组能放进字典的键、集合,或者 lru_cache 的键,这些都是列表去不了的地方。让类型本身承载这份约定,而不是靠注释说明。
python
locations = {}
locations[(40, -74)] = "北京"   # 元组作为字典键,可以正常工作
locations[[40, -74]] = "北京"   # 列表作为字典键,会抛出 TypeError

解包 ​

解包能把元组里的值一次性取出来,分别赋给各自的名字,只用一行代码。名字的数量必须和值的数量一致。用 * 可以把剩下的项都收进一个列表里。

解包适用于任何可迭代对象:元组、列表、字符串。目标名字的数量必须和可迭代对象的长度一致,除非用带星号的目标去承接一段长度可变的切片。数量不匹配会抛出 ValueError。解包是从函数取出多个返回值的惯用写法。

解包适用于任何可迭代对象(任何你能遍历的东西),不仅限于元组,它会依次遍历右侧的数据,把每个值按顺序绑定到对应的目标名字上。带星号的目标(*rest)会把剩余的项都收进一个 list,所以数量不再需要严格一致。单纯的数量不匹配会在运行时抛出 ValueError。最常用到解包的地方是在 for 语句里:for name, score in pairs 会在每次循环时解包出每一项,比手动按索引取每对数据要清晰得多。

Juno解包 解包能把元组或列表里的每个值取出来,一行代码就分别赋给各自的名字,比如 x, y = point。名字的数量必须和值的数量一致,除非你加上一个 *rest 来把剩下的都收进去。我就是从不再到处写 point[0] 和 point[1] 的那天开始,才真正弄明白这个用法的。
Juno解包 解包会把每个值一行赋给各自的名字,像 *rest 这样带星号的目标能承接一段长度可变的切片。数量不对应就会抛出 ValueError。这是从函数取出多个返回值的干净写法,不用再对结果按索引取值。
Juno解包 解包适用于任何可迭代对象,按从左到右的顺序绑定;*rest 吸收多余的部分,数量单纯不匹配则抛出 ValueError。它最值回票价的地方是 for 语句:for name, score in pairs 比按索引去取每一对要好用得多。
python
point = (10, 20)
x, y = point

print(x)   # 10
print(y)   # 20

first, *rest = [1, 2, 3, 4, 5]
# first = 1, rest = [2, 3, 4, 5]

head, *middle, tail = [1, 2, 3, 4, 5]
# head = 1, middle = [2, 3, 4], tail = 5

命名元组 ​

命名元组是一种每个位置都有名字的元组。不用再费力记住 point[0] 是 x 坐标,直接写 point.x 就行。值依然是不可变的,你只是把数字位置换成了可读的属性名。

namedtuple 会生成一个行为和元组完全一致、但额外支持按名字访问属性的类。它比完整的类更轻量、不可变,而且自解释性强。当普通元组按位置访问需要靠注释才能看懂时,就该用它。

collections.namedtuple 是一个类工厂(一个能替你构建并返回一个全新类的函数)。它返回的类本质上是带命名字段的 tuple,所以内存开销和普通元组一样,但可读性要好得多。你还能免费获得 _asdict()(转换成字典)、_replace()(由于不能直接修改元组,就复制一份并改动某个字段)以及 _fields。如果需要更多功能,比如默认值或类型标注,typing.NamedTuple 覆盖了带类型标注的场景,而一旦你需要方法或者可选的可变性,dataclasses.dataclass 是更现代的选择。

Juno命名元组 命名元组给每个位置都起了名字,所以你写的是 point.x,而不用去记 point[0] 是 x。它依然是完全不可变的,除了这点,其余用法都和普通元组一样。等你以后回头看代码时,会感谢当初起了这些名字。
Juno命名元组namedtuple 构建出一个类似元组、支持按名字访问属性的类,不可变且自解释性强,比完整的类要轻量得多。一旦普通元组的位置需要注释才能说清楚,就该换成它。再往前一步,dataclass 是更现代的升级方案。
Juno命名元组namedtuple 给你一个带命名字段、内存开销不变的元组子类,还免费附带 _replace() 和 _asdict()。一旦你想要默认值、方法或真正的类型提示,就直接换成 typing.NamedTuple 或 dataclass,别硬跟这个工厂较劲。

namedtuple 属于标准库,所以要先导入才能用:from collections import namedtuple。导入的完整讲解在模块一章。

python
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
Player = namedtuple("Player", ["name", "score", "level"])

p = Point(10, 20)
p.x    # 10
p.y    # 20

alice = Player("小美", 87, 5)
alice.name    # "小美"
alice.score   # 87

集合 ​

集合是一组唯一值的集合,不保证任何顺序。重复添加同一个值不会有任何效果:集合只会保留每一项的一份拷贝。用花括号加上元素来创建一个有内容的集合,或者用 set() 创建一个空集合。

set 是一种会自动拒绝重复值的无序集合。无论集合有多大,成员检测都是 O(1),所以一旦你需要判断某个值是否存在于一个很大的集合中,它就是正确的工具。注意:{} 创建的是空字典,不是空集合;要创建空集合请用 set()。

set 保存的是唯一且可哈希的值,它基于哈希表构建,和字典采用同一种按内容定位的结构。这带来的好处是平均 O(1) 的成员检测、插入和删除(随着集合增大,这些操作的开销保持不变,不会随规模增长而上升)。有两个后续影响要考虑:只有可哈希的值才能放进集合,所以 int、str、tuple 都可以,但 list、dict、set 不行;而且迭代顺序遵循内部的哈希位置,所以永远不要依赖它保持稳定。还有一个语法陷阱:{} 是空字典,不是空集合,要用 set()。

Juno集合 集合只保留唯一值,所以往里添加一个已经存在的东西什么都不会发生,不报错,也不会出现重复。用花括号加上元素来创建集合,但要创建空集合请用 set(),因为 {} 其实悄悄创建的是空字典。这最后一点几乎人人都会踩坑。
Juno集合 集合会自动去掉重复值,而且无论多大,成员检测都是 O(1),这就是为什么在一个很大的集合上判断"这个东西在不在里面"时它是正确的工具。留意空集合的陷阱:{} 是字典,所以要用 set()。
Juno集合 集合是唯一可哈希值组成的哈希表:O(1) 的成员检测、插入和删除,代价是只有可哈希的项才能放进去,而且迭代顺序从不稳定。别依赖那个顺序,也记住 {} 是字典,想要空集合就用 set()。
python
tags = {"python", "beginner", "tutorial"}
numbers = {1, 2, 3, 4, 5}
empty = set()    # 不是 {}(那是空字典)

重复添加同一个值不会改变集合:

python
tags.add("python")   # tags 不变,"python" 已经在里面了

什么时候该用集合 ​

集合最适合做三件事:从列表里去除重复项,快速判断某个东西是不是在一个很大的集合中,以及比较两组数据,看它们有哪些共同点、哪些不同点。

集合的使用场景主要来自三个方面:去重(插入时自动完成)、O(1) 的成员检测(相对 list 的 O(n)),以及集合运算(|、&、-、^)。当集合很大且频繁检测成员时,这种性能差异会非常明显。

三个使用场景都直接来自哈希表的特性:唯一性(插入时自动拒绝重复项)、O(1) 的成员检测(in 的开销随集合增大保持不变),以及集合运算(|、&、-、^)。成员检测是在实际代码中最有价值的一点:在一个有 10,000 个元素的集合上做 in 操作,速度和只有 10 个元素时一样快,而同样的检测在列表上是 O(n),会随规模线性变慢。当你发现自己在对一个很大的列表反复做 x in some_list 时,把它转换成集合往往就是解决办法。

Juno什么时候该用集合 集合擅长三件事:从列表里去除重复项,快速判断某个东西是不是在一个大集合里,以及比较两组数据看它们有哪些共同点、哪些不同点。如果你要做的正是这些,那集合大概就是你要的工具。
Juno什么时候该用集合 三个驱动因素:插入时自动去重、O(1) 的成员检测(相比列表的 O(n)),以及用 |、&、-、^ 做集合运算。集合越大、检测 in 越频繁,集合的优势就越明显。
Juno什么时候该用集合 唯一性、O(1) 的 in,以及集合运算,这些都源自哈希表本身。成员检测的优势才是最实际的:反复对一个大列表做 x in big_list 每次都是 O(n),所以把它一次性转换成集合通常就是解决办法。
python
# 从列表中去除重复项
raw = ["cat", "dog", "cat", "bird", "dog", "cat"]
unique = list(set(raw))   # ["cat", "dog", "bird"](顺序不保证)
python
# 快速成员检测
valid_codes = {"USD", "EUR", "GBP", "JPY"}
code = "EUR"

if code in valid_codes:    # 瞬间完成查找,即便有几千个代码也一样
    print("有效")

集合运算 ​

集合支持你在数学课上学过的相同运算:并集(存在于任一集合中的元素)、交集(两个集合共有的元素)、以及差集(一个集合有而另一个没有的元素)。Python 用运算符符号来表示这些运算,每种运算也都有对应的方法。

Python 的集合运算符和数学符号是对应的:| 表示并集,& 表示交集,- 表示差集,^ 表示对称差集。每个运算符都有对应的方法形式(.union()、.intersection() 等),而且这些方法还能接受任何可迭代对象,不仅限于集合。

运算符形式(|、&、-、^)要求两边都必须是集合,如果有一边是别的类型(比如列表),就会抛出 TypeError。方法形式(.union()、.intersection() 等)就宽松多了:它们接受任何可迭代对象并自动帮你转换,所以 a.union([1, 2]) 能正常工作,而 a | [1, 2] 会报错。当某个运算突然出错时,这个区别就是该记住的关键。此外还有就地运算形式(|=、&=、-=、^=),它们会更新左边的集合而不是返回一个新的集合,效果和调用 .update()、.intersection_update() 等方法一样。

Juno集合运算 数学课上的四种运算:| 是并集(在任一个里),& 是交集(两个都有),- 是差集(在一个里但不在另一个里),^ 是对称差集(只在一个里,不同时在两个里)。如果你更喜欢用单词而不是符号,每一种也都有对应的方法,比如 .union()。
Juno集合运算| 并集,& 交集,- 差集,^ 对称差集。方法形式(.union() 这些)效果一样,但能接受任何可迭代对象,不只是集合,这一点运算符做不到。
Juno集合运算 运算符要求两边都是集合,否则抛出 TypeError;方法形式能接受任何可迭代对象,所以 a.union([1, 2]) 能用,而 a | [1, 2] 不能。就地运算形式(|=、&=)是直接更新原集合,而不是返回一个新集合。
python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b    # {1, 2, 3, 4, 5, 6}   (并集:在任一个里的都算)
a & b    # {3, 4}               (交集:只算两个都有的)
a - b    # {1, 2}               (差集:在 a 里但不在 b 里)
b - a    # {5, 6}               (反过来的差集)
a ^ b    # {1, 2, 5, 6}        (对称差集:只在一个里,不同时在两个里)

这些运算也都有对应的方法形式:.union()、.intersection()、.difference()、.symmetric_difference()。

修改集合 ​

集合是可变的。.add() 添加一个元素。.update() 从任意列表或其他可迭代对象一次性添加多个元素。.remove() 删除某个元素,但如果元素不存在会报错。.discard() 如果元素存在就悄悄删除,不存在则什么都不做。

.add() 平均是 O(1)。.update() 接受任何可迭代对象,效果等同于在循环里不断调用 .add()。.remove() 在找不到元素时会抛出 KeyError,而当你不确定元素是否存在时,.discard() 才是安全的选择。.pop() 删除的是任意一个元素,不是"最后一个",因为集合本身没有顺序。

.add(x) 平均以 O(1) 插入一个元素;.update(iterable) 一次添加多个,效果和 |= 一样。最容易踩坑的一对是 .remove() 和 .discard():两者都是删除元素,但当元素不存在时,.remove() 会抛出 KeyError,而 .discard() 会安静地什么都不做。当元素是否存在不确定时,就该用 .discard(),这样一个缺失的元素就不必用异常去处理。.pop() 会返回并删除某个元素,但具体是哪个是不确定的:集合没有顺序,所以永远不要把 .pop() 当成取"最后一项"。

Juno修改集合.add() 添加一个元素,.update() 从一个列表或其他可迭代对象里一次添加一批。要分清楚的是删除操作:.remove() 找不到元素就报错,而 .discard() 会耸耸肩继续往下走。不确定元素是否在集合里时,用 .discard() 就能避免一个多余的报错。
Juno修改集合.add() 添加一个,.update() 从任意可迭代对象添加一批。.remove() 找不到时会抛出 KeyError;.discard() 是忽略这种情况的安全版本。.pop() 取出的是任意一个元素,不是"最后一个",因为集合本身没有顺序。
Juno修改集合.add() 是 O(1),.update() 等同于 |=。陷阱在于 .remove() 找不到元素会抛出 KeyError,而 .discard() 会安静地跳过,所以在不确定元素是否存在时默认用 .discard()。.pop() 返回的元素是不确定的;永远别把它当作最后一项来理解。
python
tags = {"python", "beginner"}

tags.add("tutorial")          # 添加一个元素
tags.update(["web", "api"])   # 从任何可迭代对象添加多个元素
tags.remove("beginner")       # 删除元素,找不到会抛出 KeyError
tags.discard("missing")       # 删除元素,找不到也不会报错
tags.pop()                    # 删除并返回一个任意元素
tags.clear()                  # 清空所有元素

不确定某个元素是否存在时,用 .discard()。

冻结集合 ​

冻结集合是一种创建之后就不能修改的集合。使用它的主要原因是:冻结集合是可哈希的,所以可以用作字典的键,或者存进另一个集合里。

frozenset 是 set 的不可变版本。它支持所有的读取操作和集合运算,但不支持修改。它的不可变性使它可哈希,也就是说它可以作为字典的键,或者作为另一个集合中的成员。

frozenset 是一种创建之后就不能再改变的 set。由于内部什么都不会变动,它拥有稳定的哈希值,本身也可哈希,所以能作为字典的键或另一个集合的成员,而普通的 set 做不到这两点。所有只读或返回新集合的操作依然可用(成员检测、集合运算符),但会修改自身的方法(add、remove 等)全都没有了。它最合适的场景是:一个在程序运行期间绝不能改变、并且可能需要嵌套在字典或集合里的常量查找表。

Juno冻结集合 冻结集合是一种一旦创建就不能再改变的集合。正是这种"锁死"状态让它可哈希,所以和普通集合不同,你可以把它当作字典的键,或者塞进另一个集合里。当你有一组固定不变、永远不该变化的允许值时,就该用它。
Juno冻结集合frozenset 是不可变版的 set:所有读取操作和集合运算都保留,只是不能修改。它的不可变性带来了可哈希性,所以它能作为字典的键或另一个集合的成员,而普通 set 在这些地方会报错。很适合用作常量查找表。
Juno冻结集合frozenset 有稳定的哈希值,所以能用在 set 用不了的地方:字典的键、另一个集合的成员。读取和集合运算都保留,修改方法全部没有了。最合适的用法是一个运行时绝不能改变的常量查找表。
python
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived")    # AttributeError,frozenset 是不可变的

该选哪种集合类型 ​

四种类型,各有明确的定位。想清楚你需要对数据做什么,答案通常就会自然浮现。

在这几种集合类型之间做选择,关键在于哪些操作对你重要,以及你的数据有哪些约束:可变性、顺序、如何处理重复值,以及查找方式。

这个选择一部分靠性能,一部分靠语义。dict 和 set 通过哈希实现平均 O(1) 的查找,所以随着数据增长,成员检测依然保持很快。list 和 tuple 按索引访问是 O(1),但成员检测是 O(n),in 的开销会随规模增长而变慢。另一个考量维度是类型本身传达的语义:tuple 是一条固定记录,它的不可变性带来了可哈希性,而 list 是长度可变的序列。当你需要一个可哈希的集合类型来嵌套进字典或集合里时,tuple 和 frozenset 是内置类型里能做到这一点的两个选择。

Juno该选哪种集合类型 四种类型,四个明确的定位:列表用于会变化的有序数据,元组用于固定记录,集合用于唯一值和快速的"在不在里面"判断,字典用于按名字查找。想清楚你要对数据做什么,通常答案就出来了。
Juno该选哪种集合类型 按数据的需求来选:可变性、顺序、是否关心重复值,以及你怎么查找数据。有序且要修改的序列用列表,固定记录用元组,唯一值和 O(1) 成员检测用集合,键值查找用字典。
Juno该选哪种集合类型 一半靠性能,一半靠语义:dict 和 set 提供 O(1) 查找,list 和 tuple 按索引是 O(1),但成员检测是 O(n)。tuple 还传递着"固定记录"的信号,并带来可哈希性。需要嵌套一个可哈希集合时,tuple 和 frozenset 就是你的两个选择。
listtuplesetdict
有序是是否是(按插入顺序)
可变是否是是
允许重复是是否否(键)
访问方式索引索引不适用键
适用场景有序且可变的序列固定记录唯一值、快速成员检测键值查找

一个快速的判断规则:

  • 需要按名字查找? → dict
  • 需要一个有序且会修改的集合? → list
  • 有一组固定不变的相关值? → tuple
  • 需要唯一值或快速的成员检测? → set

实践演练 ​

用元组存储固定记录,用集合追踪唯一值:

python
home = (39.9042, 116.4074)   # 纬度,经度
office = (39.9163, 116.3972)

home_lat, home_lon = home
print(f"家:{home_lat}, {home_lon}")

# 用集合追踪唯一访客
visitors = set()
visitors.add("小美")
visitors.add("小华")
visitors.add("小美")    # 已经在集合里了,会被悄悄忽略
visitors.add("小丽")

print(f"唯一访客数:{len(visitors)}")
print(f"小美访问过:{'小美' in visitors}")
print(f"小明访问过:{'小明' in visitors}")

用集合追踪已经处理过的内容,并计算剩余的工作:

python
already_processed = {"report_jan.csv", "report_feb.csv"}
all_files = {"report_jan.csv", "report_feb.csv", "report_mar.csv", "report_apr.csv"}

to_process = all_files - already_processed
print(f"待处理文件:{sorted(to_process)}")

for filename in sorted(to_process):
    print(f"正在处理 {filename}...")
    already_processed.add(filename)

print(f"完成。共处理:{len(already_processed)}")

用 frozenset 构建常量查找表,并结合集合运算演示 O(1) 的成员检测:

python
ALLOWED_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE"})
SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})

# frozenset 之间的集合运算返回的是普通 set
unsafe_allowed = ALLOWED_METHODS - SAFE_METHODS
print(f"非安全的允许方法:{unsafe_allowed}")

# frozenset 是可哈希的,所以能存进集合里(普通 set 做不到)
method_groups = {
    frozenset({"GET", "HEAD", "OPTIONS"}),
    frozenset({"POST", "PUT", "PATCH"}),
    frozenset({"DELETE"}),
}
print(f"方法分组数:{len(method_groups)}")

method = "POST"
print(f"允许:{method in ALLOWED_METHODS}")
print(f"安全:{method in SAFE_METHODS}")

frozenset 具备 O(1) 的查找性能,并且可以存放在任何需要可哈希类型的地方。对两个 frozenset 对象做集合运算返回的是一个普通的 set;如果要保持不可变性,把结果再包一层 frozenset() 即可。