首页
/ CPython 3.15 Sentinel 哨兵对象指南:PySentinel_Type、PySentinel_Check 与 PySentinel_New 完整解析

CPython 3.15 Sentinel 哨兵对象指南:PySentinel_Type、PySentinel_Check 与 PySentinel_New 完整解析

2026-09-06 23:41:13作者:何举烈Damon

本文以 CPython 仓库中的 sentinel C API 文档 为核心,完整讲解 Python 3.15 新增 sentinel 内建类型配套的 C 扩展 API——PySentinel_TypePySentinel_Check/PySentinel_CheckExactPySentinel_New 的语义、参数约束与失败行为,并结合 实现源码类型定义头文件官方测试用例,说明哨兵对象在 C 扩展开发中的正确用法与底层机制。

1. Sentinel 背景:为什么需要内建哨兵类型

哨兵(sentinel)是一种用"独特对象身份"而非"特殊值"来表达缺失、默认或终止状态的惯用技术。在 Python 3.15(PEP 661,见 whatsnew 3.15)之前,开发者通常手写 object() 或自定义类来充当哨兵,缺点是 repr 不可读、无法跨进程传递。3.15 起,builtins 中新增了一等公民类型的 sentinel 内建类型,sentinel(name, /, *, repr=None) 可以创建带有简洁可读表示的唯一哨兵对象。

标准库已经率先采用了这一新类型。例如 Lib/dataclasses.py 用它替换了过去匿名对象式的哨兵:

# A sentinel object to detect if a parameter is supplied or not.
MISSING = sentinel("MISSING")

# A sentinel object to indicate that following fields are keyword-only by default.
KW_ONLY = sentinel("KW_ONLY")

这样 dataclasses 在检测"参数是否显式传入"以及"字段是否为 keyword-only"时,得到的调试输出是 MISSINGKW_ONLY,而不是一长串 <object object at 0x...>

2. sentinel 类型的关键行为(源码印证)

要正确使用 C API,先必须清楚被操作对象的行为边界。以下内容均可从 Objects/sentinelobject.c 得到验证:

  • 不可子类化PySentinel_Typetp_flagsPy_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_HAVE_GCObjects/sentinelobject.c#L203-L222),没有 Py_TPFLAGS_BASETYPE,因此 class Sub(sentinel) 会抛 TypeError——这正是 C API 文档中"该类型目前不允许子类"这句话的实现依据。
  • 三个成员槽。C 结构体 sentinelobject 包含 namemodulerepr 三个 PyObject *Objects/sentinelobject.c#L13-L18),对应 __name____module__ 两个成员描述符(__name__ 只读,__module__ 可写,见 Objects/sentinelobject.c#L188-L192)。
  • repr 回退规则sentinel_reprself->repr != NULL 时返回自定义 repr,否则返回 self->nameObjects/sentinelobject.c#L152-L160)。
  • 可哈希、可比较、可 GC 追踪。类型实现了 tp_hashtp_richcompare 和完整的 tp_traverse/tp_clearObjects/sentinelobject.c#L124-L150),因此哨兵对象可以作为字典键、可以参与引用环检测。
  • 支持 | 构造类型表达式sentinel_as_number.nb_or = _Py_union_type_orObjects/sentinelobject.c#L194-L196),所以 sentinel("MISSING") | int 能得到 typing.UnionNone | sentinel 同样成立。测试用例 覆盖了 missing | intint | missingmissing | Nonemissing | list[int]missing | (int | str) 等场景,并验证 missing | missing is missingmissing | 1TypeError
  • 拷贝即同一对象__copy____deepcopy__ 都直接 Py_NewRef(self)Objects/sentinelobject.c#L162-L172),即哨兵对象在深浅拷贝后仍保持身份不变。
  • 注册为内建类型Python/bltinmodule.c#L3577 通过 SETBUILTIN("sentinel", &PySentinel_Type); 将其挂到 builtins 模块。

3. PySentinel_Type:类型对象

C API 文档(Doc/c-api/sentinel.rst)声明的第一个符号:

PyAPI_DATA(PyTypeObject) PySentinel_Type;   /* versionadded: 3.15 */

PyTypeObject 实例就是 Python 侧的 sentinel 类型对象,"This is the same object as sentinel"——即 C 端的 PySentinel_Type 与 Python 端 builtins.sentinel 是同一个对象。在 C 扩展中的典型用途:

  • 调用 Py_IS_TYPE(op, &PySentinel_Type) 做精确类型判断(见下节宏);
  • 调用 Py_INCREF((PyObject*)&PySentinel_Type) 后在 C 代码里引用 sentinel 类型本身;
  • 作为 PyType_ReadyPyObject_IsInstance 等通用 API 的实参。

声明位置在 Include/cpython/sentinelobject.h#L10。注意该头文件整体被 #ifndef Py_LIMITED_API 包裹,因此这些符号只面向稳定 ABI 之外的全量 API 扩展。

4. PySentinel_Check 与 PySentinel_CheckExact:类型检查

文档定义了两个检查函数,语义分别对应"是否为 sentinel 或其子类型"与"是否严格是 sentinel":

API 语义 当前实现
PySentinel_Check(PyObject *o) osentinel 对象或其子类型时返回真;"当前该类型不允许子类,所以此检查是精确的;未来版本可能允许子类型";函数总是成功 等价于 PySentinel_CheckExact
PySentinel_CheckExact(PyObject *o) osentinel 对象而非子类型时返回真;"当前该类型不允许子类;未来版本可能允许子类型";函数总是成功 Py_IS_TYPE(o, &PySentinel_Type)

Include/cpython/sentinelobject.h#L12-L15 中可以看到具体实现:

#define PySentinel_CheckExact(op) Py_IS_TYPE((op), &PySentinel_Type)

/* Alias as long as subclasses are not allowed. */
#define PySentinel_Check(op) PySentinel_CheckExact(op)

两点工程含义:

  1. 两者目前都只是宏,开销等同一次类型指针比较,"This function always succeeds" 意味着不会设置异常,可以安全地在热路径中调用。
  2. 由于 PySentinel_Check 被实现为 PySentinel_CheckExact 的别名(/* Alias as long as subclasses are not allowed. */),一旦未来 Python 版本允许 sentinel 子类,两者的语义将自动分叉:Check 继续兼容子类,CheckExact 收紧为精确匹配。编写扩展时应按意图选择:需要"广义哨兵"用 PySentinel_Check,需要"严格内建哨兵"用 PySentinel_CheckExact。这一补充 API 的引入也记录在 NEWS 条目 中。

C 扩展中使用检查宏的典型模式:

#include "Python.h"
#include "cpython/sentinelobject.h"

PyObject *
get_default(PyObject *arg)
{
    if (arg == NULL) {
        return Py_None;
    }
    if (PySentinel_Check(arg)) {
        /* 调用方传入了哨兵,表示"未提供" */
        return Py_NewRef(Py_None);
    }
    return Py_NewRef(arg);
}

5. PySentinel_New:创建哨兵对象

文档声明的原型与参数契约:

PyObject *PySentinel_New(const char *name,
                         const char *module_name,
                         const char *repr);
参数 约束 行为
name 不得为 NULL 设置为 __name__
module_name 可以为 NULL NULL__module__ 被设为 None
repr 可以为 NULL NULLrepr() 回退返回 __name__

返回值:成功时返回新建的 sentinel 对象;失败时返回 NULL 并设置异常。

对照 PySentinel_New 的实现Objects/sentinelobject.c),可以看到文档契约与代码一一对应:

PyObject *
PySentinel_New(const char *name, const char *module_name, const char *repr)
{
    PyObject *name_obj = PyUnicode_FromString(name);   // name 为 NULL 时此处失败
    ...
    PyObject *module_obj = module_name == NULL
        ? Py_None                                       // module_name == NULL → __module__ = None
        : PyUnicode_FromString(module_name);
    ...
    PyObject *sentinel = sentinel_new_with_module(
        &PySentinel_Type, name_obj, module_obj, repr_obj);
    ...
}

注意一个与文档措辞的细微差别:name "must not be NULL" 是前置契约——传 NULL 会经由 PyUnicode_FromString(NULL) 触发 SystemError 类的失败路径(实现上以失败返回并设置异常),而不是一个专门检查。因此在扩展代码中应始终传入非 NULLname

5.1 与 Python 侧 sentinel(...) 的差异

Python 层的 sentinel.__new__ 签名是 sentinel(name, /, *, repr=None)(见 sentinel_new_impl):__module__ 由解释器自动推断——caller() 从当前帧取所属函数模块(Objects/sentinelobject.c#L31-L47),而不是把 None 交给用户。也就是说:

  • Python 里写 sentinel("MISSING"),其 __module__调用所在模块的名字,这保证了该对象可被 pickle(见下节);
  • C 扩展里调用 PySentinel_New 时没有"当前调用者模块"可推断,因此必须显式提供 module_name,或传 NULL 接受 __module__ = None(此时对象不可 pickle)。

5.2 Pickle 支持及其前置条件

文档中关于序列化的一段必须完整理解:

For pickling to work, module_name must be the name of an importable module, and the sentinel must be accessible from that module under a path matching name. Pickle treats name as a global variable name in module_name (see object.__reduce__).

机制在 sentinel_reduce__reduce__ 直接返回 self->name,pickle 随即按"模块全局变量"的常规路径去 module_name 中按 name 查找对象。由此得出使用规则:

  1. 模块必须可导入module_name 是真实模块名(可含包前缀,如 "mylib.constants");
  2. 名字必须与模块属性路径一致:如果哨兵是模块级变量 mylib.constants.MISSING,则 name 应传 "MISSING";如果它挂在类上(如 SomeClass.MISSING),name 需传对应的属性路径;
  3. 两条同时满足时,pickle.loads(pickle.dumps(x)) is x 成立(同一模块加载下按名字解析回同一对象);否则 pickle.dumps 会抛 pickle.PicklingError

测试 完整验证了这两个方向:模块级 A_SENTINEL 与类属性 SentinelContainer.CLASS_SENTINEL所有 pickle 协议下往返后 is 同一对象;而一个从未被任何模块以 MISSING 名字暴露的 sentinel("MISSING"),在所有协议下 pickle.dumps 均抛 PicklingError

5.3 在 C 扩展中创建哨兵的示例

假设你要在扩展中定义一个模块级哨兵并在函数里使用它,可参考标准库的模式(如 dataclasses 在 Python 层等价地写 MISSING = sentinel("MISSING")):

static PyObject *g_missing = NULL;   /* 模块级哨兵,module init 时创建一次 */

/* 模块初始化阶段:name 必须与模块中可访问的属性路径一致 */
g_missing = PySentinel_New("MISSING", "__myext", "MISSING");
if (g_missing == NULL) {
    return -;   /* 异常已设置,模块初始化失败 */
}
PyModule_AddObject(m, "MISSING", g_missing);  /* 转移引用 */

/* 后续使用:以身份比较判断"参数是否提供" */
static PyObject *
myext_do_stuff(PyObject *module, PyObject *arg)
{
    PyObject *result;
    if (PySentinel_Check(arg)) {      /* 调用方传入了 MISSING 哨兵 */
        result = PyLong_FromLong(-1);
    } else {
        result = PyObject_Str(arg);   /* 示例处理 */
    }
    return result;
}

要点回顾:

  • name"MISSING",与 PyModule_AddObject 注册的属性名一致——这是可 pickle 的前提;
  • module_name"__myext"(扩展模块的真实 __name__)——保证 __module__ 指向可导入模块;
  • 第三个参数显式给了 "MISSING" 作为自定义 repr;若改为 NULLrepr() 会回退返回 __name__,即同样显示 MISSING

6. 边界行为速查

以下行为全部有测试或源码依据,便于在扩展开发中直接引用:

  • 构造错误:sentinel()(缺 name)、位置参数多于 1 个、repr 传非 str(如 repr=42)均抛 TypeError测试);
  • 两个同 name 的哨兵身份不同sentinel("MISSING") is not sentinel("MISSING")测试)——"哨兵"的价值来自身份而非相等性;
  • sentinel.__flags__Py_TPFLAGS_IMMUTABLETYPEPy_TPFLAGS_HAVE_GC、不含 Py_TPFLAGS_BASETYPE,禁止子类化(测试);
  • 即使 namestr 子类(可能形成引用环),对象也被 GC 追踪且可被正常回收(测试,印证 tp_traverse/tp_clear 实现的正确性);
  • 类型不可被动态添加属性(sentinel.attribute = "value"AttributeError,对应 IMMUTABLETYPE 标志)。

7. 小结

sentinel C API(3.15+,见 Doc/c-api/sentinel.rst)只有三个符号,但契约完整:PySentinel_Type 是 Python sentinel 类型的同一对象;PySentinel_Check/PySentinel_CheckExact 是无副作用的检查宏,当前互为别名、为未来可能的子类化预留了语义分叉;PySentinel_New 创建对象时须牢记"name 非 NULL、module_name 决定 __module__ 且为 pickle 前提、repr 为 NULL 时回退 __name__"三条契约。结合 Objects/sentinelobject.c 的实现细节与 test_builtin 的 sentinel 测试,扩展开发者可以安全地在 C 层创建、检查并利用哨兵对象表达"未提供/缺失"状态,同时保证其在 Python 侧拥有可读 repr、可哈希身份和(满足命名条件时的)可序列化能力。

适用前提:以上 API 仅存在于 Python 3.15 及之后的开发版本,且 Include/cpython/sentinelobject.h 位于全量(非 limited API)头文件之下,扩展需按完整 C API 构建。

登录后查看全文
热门项目推荐
相关项目推荐