0%

Python 3.15 新内置函数:sentinel

转载声明:本文翻译转载自 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
2
NOTHING = sentinel("NOTHING")
print(NOTHING) # NOTHING

哨兵值的名称会被用作它的默认字符串表示(repr)。

你也可以提供一个第二个命名参数,用作哨兵值的表示形式:

1
2
NOTHING = sentinel("NOTHING", repr="__main__.NOTHING")
print(NOTHING) # __main__.NOTHING

虽然运行时对此没有任何强制限制,但哨兵值的名称与它所赋值的变量名应当保持一致:

1
2
3
4
5
6
7
# 正确:
NOTHING = sentinel("NOTHING")

# 错误:
NOTHING = sentinel("nothing")
N = sentinel("NOTHING")
BANANAS = sentinel("APPLES")

如果不一致,linter 和类型检查器可能会报错。由于哨兵值通常是不会再去更新的常量,它们的名字一般也约定俗成为全大写。

哨兵值通常用作占位符,最常见的操作就是检查某个值是否是某个特定的哨兵值。例如,设想一个 find 函数,它在可迭代对象中查找满足给定谓词(predicate)的值:

1
2
3
4
5
6
7
NOTHING = sentinel("NOTHING")

def find(iterable, predicate):
for value in iterable:
if predicate(value):
return value
return NOTHING

注意,这里的默认返回值不能是 None,因为在某些情况下 find(iterable, predicate) 确实可能返回值 None。这就意味着需要一个专门的占位值,所以要用到内置的 sentinel。

要判断 find 函数返回的是不是占位值,应当使用比较运算符 is 或 is not:

1
2
if (value := find(iterable, predicate)) is not NOTHING:
print(f"A value was found: {value}")

类型标注中的哨兵值

在类型标注(typing)的语境下,哨兵值可以像 None 一样,作为一种独立的类型来使用。还是上面那个 find 函数,它的返回类型可以写成 -> ... | NOTHING:

1
2
3
4
5
6
7
8
9
from collections.abc import Callable, Iterable

NOTHING = sentinel("NOTHING")

def find[V](iterable: Iterable[V], predicate: Callable[[V], bool]) -> V | NOTHING:
for value in iterable:
if predicate(value):
return value
return NOTHING

上面的代码片段把 find 函数定义为一个接受「某种类型 V 的值的可迭代对象」的函数。然后,这个函数要么返回一个类型为 V 的值(如果找得到),要么返回哨兵值 NOTHING。

来自 Textual 的真实案例

作为哨兵值应用场景的又一个例子,来看看开源项目 Textual 中的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class UnusedParameter:
"""Helper type for a parameter that isn't specified in a method call."""

UNUSED = UnusedParameter()
"""Sentinel for method signatures."""

class ProgressBar(Widget):
"""A progress bar widget."""

...

def update(
self,
*,
total: None | float | UnusedParameter = UNUSED,
progress: float | UnusedParameter = UNUSED,
advance: float | UnusedParameter = UNUSED,
) -> None:
"""Update the progress bar with the given options.

Example:
progress_bar.update(
total=200, # Set new total to 200 steps.
progress=50, # Set the progress to 50 (out of 200).
)
"""

if not isinstance(total, 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NOTHING = sentinel("NOTHING")

class ProgressBar(Widget):
"""A progress bar widget."""

...

def update(
self,
*,
total: None | float | NOTHING = NOTHING,
progress: float | NOTHING = NOTHING,
advance: float | NOTHING = NOTHING,
) -> None:
"""Update the progress bar with the given options."""

if total is not 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 新特性。

参考资料

扫码加入技术交流群🖱️
QR code