转载声明:本文翻译转载自 mathspp.com 的文章 sentinel built-in,作者 Rodrigo Girão Serrão,原文发布于 2026 年 10 月 13 日。本文为翻译转载,版权归原作者所有。
Python 3.15 引入了新的 sentinel 内置函数。sentinel 内置函数可用于创建占位值:
1 | NOTHING = sentinel("NOTHING") |
由 sentinel 创建的占位值,是在 None 无法胜任的场合提供默认参数或返回值的更地道的方式。
「Python 3.15 的 15 天」系列
为了庆祝 Python 3.15 发布,作者将在接下来的 15 个工作日里,每天讲解一个 Python 3.15 的新特性——解释清晰、附带示例,让你不必自己去翻变更日志。
创建与使用哨兵值
内置的 sentinel 接受一个字符串参数,作为哨兵值的名称:
1 | NOTHING = sentinel("NOTHING") |
哨兵值的名称会被用作它的默认字符串表示(repr)。
你也可以提供一个第二个命名参数,用作哨兵值的表示形式:
1 | NOTHING = sentinel("NOTHING", repr="__main__.NOTHING") |
虽然运行时对此没有任何强制限制,但哨兵值的名称与它所赋值的变量名应当保持一致:
1 | # 正确: |
如果不一致,linter 和类型检查器可能会报错。由于哨兵值通常是不会再去更新的常量,它们的名字一般也约定俗成为全大写。
哨兵值通常用作占位符,最常见的操作就是检查某个值是否是某个特定的哨兵值。例如,设想一个 find 函数,它在可迭代对象中查找满足给定谓词(predicate)的值:
1 | NOTHING = sentinel("NOTHING") |
注意,这里的默认返回值不能是 None,因为在某些情况下 find(iterable, predicate) 确实可能返回值 None。这就意味着需要一个专门的占位值,所以要用到内置的 sentinel。
要判断 find 函数返回的是不是占位值,应当使用比较运算符 is 或 is not:
1 | if (value := find(iterable, predicate)) is not NOTHING: |
类型标注中的哨兵值
在类型标注(typing)的语境下,哨兵值可以像 None 一样,作为一种独立的类型来使用。还是上面那个 find 函数,它的返回类型可以写成 -> ... | NOTHING:
1 | from collections.abc import Callable, Iterable |
上面的代码片段把 find 函数定义为一个接受「某种类型 V 的值的可迭代对象」的函数。然后,这个函数要么返回一个类型为 V 的值(如果找得到),要么返回哨兵值 NOTHING。
来自 Textual 的真实案例
作为哨兵值应用场景的又一个例子,来看看开源项目 Textual 中的这段代码:
1 | class UnusedParameter: |
ProgressBar.update 方法用于更新进度条的状态。比如,调用 update(total=None) 会把进度条切换为那种持续刷新的类型,用来表明 UI 没有卡死。由于 None 是 total 的一个合法参数值,所以它不能用作默认值。
由于 Textual 是完全类型化的,用 object() 作为默认值同样不可行——如果把 total 的类型写成 total: None | float | object,那么像 update(total="bananas") 这样的调用就不会被类型检查器抓住,因为字符串 "bananas" 的类型就是 object。
在 Python 3.15 引入 sentinel 之前,解决办法是专门定义一个 UnusedParameter 类,再创建该类的一个实例 UNUSED。
有了 Python 3.15 的新内置 sentinel 之后,代码就变成了:
1 | NOTHING = sentinel("NOTHING") |
新版本的代码直白得多,因为意图从一开始就一目了然。update 方法内部的代码也随之变得更干净,虽然从上面摘录的代码里看不出来。
创建多个哨兵值
可以通过多次调用内置的 sentinel 来创建多个哨兵值。在某些场景下,需要用它们来区分不同语境下的不同占位符。
如果要定义多个占位符,它们应当使用不同的名字。同名的哨兵值会造成混乱——它们在 is 检查下是不同的对象,看起来却一模一样:
1 | print(sentinel("NOTHING") is sentinel("NOTHING")) # False |
上面的代码片段说明,可以创建两个同名的哨兵值,但它们会被视为不同的对象。如果把它们赋给(不同语境下的)同名变量,就很可能造成混淆。
优先使用 None
新增内置 sentinel 是为了覆盖那些 None 无法胜任占位符的场合。因此,目前的共识是:能用 None 作占位符时,仍然默认用 None;只有当 None 用不了时,才动用 sentinel。
Python 3.15 的精彩远不止于此
从两个新内置的 frozendict 和 sentinel,到惰性导入(lazy imports)、包启动配置文件、typing 改进,以及更多色彩支持,Python 3.15 还有很多值得一看的东西。如果你想进一步了解 Python 3.15 的新特性,可以看看作者正在连载的「Python 3.15 的 15 天」系列——15 天里每天讲解一个 Python 3.15 新特性。
参考资料
- PEP 661 – Sentinel Values(哨兵值),https://peps.python.org/pep-0661/ [原文访问日期:2026-10-13];