跳至主要内容

Python frozenset:它为何存在,以及何时该用

了解 frozenset 如何为 Python 容器带来不可变性与可哈希性,并学习何时选择它而不是普通 set。
· 7分钟 阅读

使用 AI 探索

如果您用过 Python 的集合(set),很容易忽略 frozenset。它看起来几乎一样,用起来也几乎一样,但大多数人只有在有明确理由时才会用它。而这个理由通常归结为一件事:不可变性。

在 Python 中,frozenset 是集合的不可变、可哈希版本。一旦创建,您无法向其中添加、删除或以任何方式更改它。起初这听起来像种限制,但在实践中,它解锁了普通集合无法支持的行为,尤其当您在意安全性、可预测性,或需要将集合用作字典键时。

本教程旨在让 frozenset 不再抽象。我们将介绍它是什么、与普通集合有何不同,以及在什么情况下它不是冷门替代品,而是恰当工具。

Python 中的 frozenset 是什么?

从基本层面看,frozenset 是 Python 的不可变集合版本。与普通集合一样,它存储无序且唯一的元素。不同之处在于,一旦创建,frozenset 的内容就被锁定。您仍然可以进行成员检查、对其迭代,以及执行并集、交集等集合运算。您不能做的是就地修改。这个固定特性正是它的意义所在。

您不会因为 frozenset 比 set 能做得更多而选它,而是因为它有意地“能做得更少”,而这种“更少”在某些方面恰恰更有用,是可变集合无法比拟的。

Frozenset 与其他 Python 容器的比较

理解 frozenset 是什么之后,更实际的问题是:与 Python 提供的其他容器类型相比,何时使用它更合适。在实践中,最重要的两个比较对象是 set 和 tuple,因为这三者都能容纳多个值,但表达的意图截然不同。

Frozenset 与 set

两者的根本区别在于可变性。

一个 set 是可变的。您可以在程序运行时添加、移除元素并更新它。这种灵活性使 set 很适合用于收集值、过滤数据或跟踪随时间变化的状态等任务。

一个 frozenset 是不可变的。一旦创建,其内容就固定不变。由于 Python 可以依赖这一保证,frozenset 是可哈希的,而普通 set 不是。这使得 frozenset 可以用作字典键,或存放在另一个集合中。

在实践中,选择通常取决于意图:

  • 当集合需要改变时,使用 set。
  • 当集合表示一个不应被修改的固定分组时,使用 frozenset。

如果您在想“这个集合初始化后就不该再变更”,那往往意味着 frozenset 更合适。

Frozenset 与 tuple

frozenset 和 tuple 都是不可变的,这一开始会让它们看起来可以互换。区别在于它们所表达的含义。

tuple 是有序且基于位置的。每个元素的含义与其出现的位置相关:

point = (10, 20)

在这里,顺序很重要。交换两个值会改变含义。

相比之下,frozenset 是无序且基于成员关系的。它表示的是一个顺序无关、且不允许重复的分组:

roles = frozenset({"admin", "editor"})

这种区分对表达清晰很重要。如果容器表示的是权限、标签、类别或标志的逻辑分组,frozenset 往往比 tuple 更能清晰传达意图;如果位置或序列有意义,则应选 tuple。

为何不可变性很重要

这正是让 frozenset 超越理论层面的地方。当一个对象无法改变时,您的心智模型会更简单。您不用担心是否有其他函数修改了它,或其内容是否仍然符合预期。一旦创建,就一直有效。

Python 对此理念的依赖很深。字符串、数字、tuple 和 frozenset 都是不可变的,原因相同:可安全复用、可安全共享,并可安全地用作字典键或作为其他集合的元素。

这也带来实用的安全性。在大型代码库中,意外修改是常见的错误根源。如果某个值表示不应改变的规则、配置或逻辑分组,让它不可变不仅是实现细节,也是在向读代码的人发出信号。

这就是 frozenset 的定位:集合的表达力与不可变对象的可预测性相结合。

在 Python 中创建 frozenset

在理解了不可变性之后,接下来就是实际问题:在真实代码里如何创建 frozenset?

从可迭代对象创建 frozenset

最常见的方式是将一个可迭代对象传入 frozenset() 构造函数:

numbers = frozenset([1, 2, 3, 3, 4])
print(numbers)

与普通集合一样,重复元素会被自动去重。

一个常见模式是先用可变集合构建,再“锁定”它:

allowed_roles = {"admin", "editor", "viewer"}
allowed_roles = frozenset(allowed_roles)

在构建阶段数据保持灵活,一旦它代表的是固定规则,就转为不可变。从那一刻起,Python 会强制执行这一意图。

您也可以直接从另一个 set 创建 frozenset:

original = {1, 2, 3}
frozen = frozenset(original)

此时,original 仍然可以变化,但 frozen 永远不会变。

空 frozenset 与常见陷阱

创建空 frozenset 容易让人犯错,因为 Python 没有为它提供字面量。下面的写法不会创建空集合:

empty = {}

那是一个空字典。frozenset 没有 {} 的等价字面量。唯一正确的方式是:

empty_frozen = frozenset()

在代码里看到 frozenset() 是一个清晰信号:这是一个有意为空、不可变的集合,而不是将来要填充的容器。

使用 frozenset 能做与不能做的事

这里有个简单的经验法则:读取类操作可以;修改类操作不可以。

支持的 frozenset 操作

您可以进行成员检查:

roles = frozenset({"admin", "editor"})
"admin" in roles  # True

您也可以执行标准集合运算。这些操作不会修改原始 frozenset;它们会返回新的集合:

a = frozenset({1, 2, 3})
b = frozenset({3, 4, 5})

a | b   # 并集
a & b   # 交集
a - b   # 差集

这是一个重要细节。不可变并不意味着“被困住”,它只意味着原对象保持不变。

不支持的操作

任何会修改集合的操作都不可用:

roles = frozenset({"admin", "editor"})
roles.add("viewer")

这会抛出 AttributeError。像 add()、remove()、discard() 这样的方法在 frozenset 上根本不存在。

当您遇到这些错误时,通常意味着您要么应该使用普通 set,要么应该在工作流程的后续阶段再将数据“冻住”,而不是太早。

Python frozenset 与可哈希性

这正是所有点连成线的地方。由于 frozenset 无法改变,Python 可以安全地为其计算哈希值。这意味着它能用在普通 set 不被允许的地方。

将 frozenset 用作字典键

在 Python 中,字典键必须是可哈希的。这就是为什么列表和 set 不能作为键:

data = {}
data[{1, 2, 3}] = "value"  # TypeError

frozenset 可以,因为其内容是固定的:

data = {}
data[frozenset({1, 2, 3})] = "value"

这种模式在缓存、权限和功能开关中很常见——只要键本身是一个集合即可。要更深入理解键为何如此工作,DataCamp 关于 Python 字典 的教程是一个有用的参考。

由于顺序不重要,frozenset({"read", "write"}) 与 frozenset({"write", "read"}) 表示同一个键,这往往正是您想要的。

在其他数据结构中使用 frozenset

可哈希性也意味着 frozenset 可以放入其他不可变或基于哈希的结构中:

groups = {
    frozenset({"admin", "editor"}),
    frozenset({"viewer"}),
}
Or as part of a composite key:
key = (user_id, frozenset(user_roles))

这些模式在记忆化、配置系统和缓存中很常见——在这些地方,稳定性比灵活性更重要。

frozenset 的常见用例

到这一步,frozenset 应该更像是一件有意为之的工具,而不是 Python 的偏门角落。

1. 表示固定集合

frozenset 天然适合那些不应改变的集合:

  • 用户权限
  • 功能开关
  • 支持的文件类型
  • 允许的状态或转换
ADMIN_PERMISSIONS = frozenset({"read", "write", "delete"})

在这里使用 frozenset 能清晰表达意图,并防止意外修改。

2. 去重与比较

当顺序无关时,frozenset 简化了相等性检查:

a = frozenset(["red", "green", "blue"])
b = frozenset(["blue", "green", "red"])

a == b  # True

它也让“集合的集合”的去重变得容易:

groups = [
    frozenset({"a", "b"}),
    frozenset({"b", "a"}),
    frozenset({"c"}),
]
unique_groups = set(groups)

无需自定义比较逻辑。

常见错误与误解

围绕 frozenset 的大部分困惑,源于将其当作列表来用,或假定所有不可变容器的行为都相同。

期望 frozenset 像列表那样工作

frozenset 是无序的。您不能通过索引访问,也不能依赖打印时看到的顺序。如果需要按位置访问,那么用错了结构。

如果为了展示需要稳定的顺序,请显式转换:

sorted_roles = sorted(roles)

混淆 frozenset 与 tuple 的不可变性

tuple 与 frozenset 都是不可变的,但表达的意图不同。

  • tuple 用于建模结构与位置。
  • frozenset 用于建模无序的成员关系。

选对结构能让代码更清晰,也更不易出错。

结语

Frozenset 是 Python 容器工具箱中小而实用的一员。它提供集合的语义——唯一、无序的元素,同时加入不可变与可哈希的保证。

这种组合非常适合特定场景:表示固定分组、将集合用作字典键,或在大型代码库中构建更安全的数据结构,防止意外修改引入缺陷。在这些情况下,frozenset 往往比普通 set 更清晰、更可靠。

同时,它并不是通用替代品。如果集合需要随时间变化,可变的 set 更合适;如果顺序或位置含义重要,tuple 更恰当。只有当它的约束与您建模的问题相契合时,frozenset 才能发挥最佳作用。

只要用得恰到好处,它就是一件能让 Python 代码更可预测、更易于推理的简单工具。

Oluseye Jeremiah

作者

Oluseye Jeremiah

专注于人工智能、机器学习与数据科学的技术写作者,擅长将复杂概念讲解得清晰易懂。

常见问题

在 Python 中,frozenset 用来做什么?

当您需要一个无序且元素唯一、并且不应发生变化的集合时,就会用到 frozenset。常见用例包括字典键、配置值、权限集合以及缓存键。

创建后还能修改 frozenset 吗?

不能。一个 frozenset 一旦创建,就无法修改。像 add()、remove() 或 discard() 等方法不可用。任何合并或比较 frozenset 的操作都会返回一个新的 set 或 frozenset。

为什么 frozenset 可哈希而 set 不可哈希?

frozenset 可哈希是因为它不可变。由于其内容不能改变,Python 可以安全地计算并复用其哈希值。普通 set 是可变的,如果可哈希就会破坏字典或集合的内部机制。

frozenset 比 set 更快吗?

在查找性能上,frozenset 与 set 相似。主要差别不在速度,而在行为:frozenset 用不可变性换取安全性与可哈希性,而非性能提升。

什么时候不该使用 frozenset?

当集合需要随时间变化、当顺序重要、或当您需要索引等列表式行为时,应避免使用 frozenset。在这些情况下,通常 set、list 或 tuple 更合适。

主题