Python 3.15 sentinel 内置类型增强:repr 参数与可写module全解析
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
导读
本文围绕 CPython 仓库中sentinel内置类型的最新变更展开:自 Python 3.15(PEP 661)引入sentinel类型后,本次 NEWS 更新为其新增了repr=构造参数,并将__module__属性改为可写。文章以官方文档与源码实现为主线,结合 Objects/sentinelobject.c 与 Lib/test/test_builtin.py 中的测试用例,完整讲解 sentinel 的创建、属性、比较语义、类型提示、pickle 支持与 C API 用法,帮助读者掌握这一标识缺失值/哨兵值的最佳实践。
变更概览:NEWS 条目解读
本次变更记录在 Misc/NEWS.d/next/Core_and_Builtins/2026-05-10-16-43-50.gh-issue-148829.gscS14.rst,原文如下:
sentinelobjects now support arepr=argument and their__module__attribute is writable.
即两项能力增强:
sentinel()构造时支持传入repr=关键字参数,自定义对象的字符串表示;sentinel实例的__module__属性由只读改为可写,允许手动修正或重置模块归属信息。
这两项能力分别由 Objects/sentinelobject.c 中的参数解析逻辑与 Objects/sentinelobject.c 中PyMemberDef的属性读写权限定义(0表示可读写)提供底层支撑。
sentinel 类型是什么:为什么需要它
在 Python 编程中,"哨兵值"(sentinel value)用于表示"未提供""缺失""无默认值"等特殊状态。常见做法是使用None,但当None本身是合法参数值时就会产生歧义。sentinel类型正是为此而生:每个 sentinel 对象都是唯一的,只与自身相等,适合与is运算符配合使用。
从 Doc/library/functions.rst 的官方定义看,其签名与语义如下:
sentinel(name, /, *, repr=None)name:必填的位置参数,必须是str,默认用作对象的表示;repr:可选关键字参数,用于自定义表示;sentinel类型不支持子类化;- 对象的浅拷贝与深拷贝都返回其自身;
- 对象为真值(truthy),只与自身相等。
在标准库中,sentinel 已被广泛采用,例如 Lib/dataclasses.py 中的MISSING = sentinel("MISSING")与KW_ONLY = sentinel("KW_ONLY"),以及 Lib/functools.py 中的_initial_missing = sentinel('_initial_missing')。这也说明本次repr与__module__增强对标准库自身的可读性维护同样有实际价值。
基础用法:创建与表示
最基本的创建方式直接传入名称字符串:
>>> MISSING = sentinel("MISSING") >>> MISSING MISSING默认情况下repr()与str()都返回name本身。从源码看,这一行为由 Objects/sentinelobject.c 的sentinel_repr函数实现:若内部repr字段非空则优先返回它,否则返回name。
核心增强一:repr= 参数
基本用法
>>> MISSING = sentinel("MISSING", repr="<MISSING>") >>> MISSING <MISSING> >>> str(MISSING) '<MISSING>'设置repr=后,repr()与str()都会返回自定义字符串,这在调试输出、日志中需要更明确提示时非常有用。显式传入repr=None则与不传等价,仍回退到name。
参数校验
repr参数必须为str或None,否则抛出TypeError。这一校验逻辑位于 Objects/sentinelobject.c:
if (repr == Py_None) { repr = NULL; } else if (!PyUnicode_Check(repr)) { _PyArg_BadArgument("sentinel", "argument 'repr'", "str or None", repr); return NULL; }对应的测试用例位于 Lib/test/test_builtin.py:
def test_sentinel_repr(self): with_repr = sentinel("WITH_REPR", repr="custom") without_repr = sentinel("WITHOUT_REPR", repr=None) self.assertEqual(repr(with_repr), "custom") self.assertEqual(repr(without_repr), "WITHOUT_REPR") self.assertEqual(str(with_repr), "custom") self.assertEqual(str(without_repr), "WITHOUT_REPR") with self.assertRaisesRegex(TypeError, "repr.*str or None"): sentinel("BAD_REPR", repr=42)底层数据结构
sentinel 对象内部通过三个字段保存信息,见 Objects/sentinelobject.c:
typedef struct { PyObject_HEAD PyObject *name; PyObject *module; PyObject *repr; } sentinelobject;对象使用 GC 分配器创建并参与垃圾回收追踪(Py_TPFLAGS_HAVE_GC),repr字段为可空引用,构造时通过Py_XNewRef持有(Objects/sentinelobject.c)。
核心增强二:可写的module属性
语义变化
此前__module__与__name__一样只读;本次变更后__module__变为可写,__name__仍保持只读。成员属性表定义于 Objects/sentinelobject.c:
static PyMemberDef sentinel_members[] = { {"__name__", Py_T_OBJECT_EX, offsetof(sentinelobject, name), Py_READONLY}, {"__module__", Py_T_OBJECT_EX, offsetof(sentinelobject, module), 0}, {NULL} };其中Py_READONLY表示只读,0表示可读写。
行为示例
>>> missing = sentinel("MISSING") >>> missing.__module__ '__main__' >>> missing.__module__ = "changed" >>> missing.__module__ 'changed' >>> del missing.__module__ # 允许删除 >>> missing.__module__ # 删除后再访问抛出 AttributeError测试用例见 Lib/test/test_builtin.py,其中还验证了__name__赋值会抛出AttributeError,而__module__可赋值、可删除。
为何需要可写module
__module__参与 pickle 的定位逻辑:只有位于可导入模块全局作用域、且名称匹配的 sentinel 才能被序列化。若对象在模块间移动或需要在序列化时指定归属模块,可写属性就提供了修正手段(详见下文 pickle 一节)。
默认模块推断机制
创建 sentinel 时若不显式指定模块,解释器会自动推断调用方所在模块,这一逻辑由 Objects/sentinelobject.c 的caller()函数完成:它读取当前线程状态中的活动帧,获取对应函数对象并调用PyFunction_GetModule得到模块名;若当前不在函数帧内或函数无模块归属,则返回None。
因此,在模块顶层创建时:
# 假设此代码位于 mypackage/constants.py MISSING = sentinel("MISSING") assert MISSING.__module__ == "mypackage.constants"比较语义与唯一性
sentinel 对象为真值(truthy),且只与自身相等(==比较基于身份)。官方文档明确指出其设计意图是与is运算符配合使用:
>>> missing = sentinel("MISSING") >>> other = sentinel("MISSING") >>> missing is other False >>> missing == missing True >>> missing == other False >>> bool(missing) True即使两个 sentinel 使用相同的name,它们仍是不同对象——这是区别于普通字符串常量的关键。相关断言见 Lib/test/test_builtin.py。
不可子类化与类型标志
sentinel是一个不可变的最终类型:不可作为基类被继承,类型对象本身也不允许设置属性。类型定义于 Objects/sentinelobject.c:
PyTypeObject PySentinel_Type = { ... .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_HAVE_GC, .tp_richcompare = _Py_BaseObject_RichCompare, ... };测试用例验证了sentinel.__flags__中包含IMMUTABLETYPE与HAVE_GC标志,且不含BASETYPE(Lib/test/test_builtin.py):
with self.assertRaises(TypeError): class SubSentinel(sentinel): pass拷贝与序列化行为
拷贝返回自身
__copy__与__deepcopy__均直接返回对象自身(Objects/sentinelobject.c),因此:
import copy missing = sentinel("MISSING") assert copy.copy(missing) is missing assert copy.deepcopy(missing) is missingpickle 支持(模块级与类级)
根据 Doc/library/functions.rst 的说明,pickle 支持是有条件的:
- 位于模块全局作用域、且变量名与
name匹配的 sentinel 可被序列化; - 位于类作用域、名称与 sentinel 的限定名(qualified name)匹配的也可序列化;
- 定义在函数作用域内的 sentinel 不可序列化。
序列化后身份保持不变:
import pickle PICKLABLE = sentinel("PICKLABLE") assert pickle.loads(pickle.dumps(PICKLABLE)) is PICKLABLE class Cls: PICKLABLE = sentinel("Cls.PICKLABLE") assert pickle.loads(pickle.dumps(Cls.PICKLABLE)) is Cls.PICKLABLE这一行为由 Objects/sentinelobject.c 的__reduce__实现支撑:它返回name字符串,pickle 将name视为模块__module__中的全局变量名进行查找还原。
对应测试见 Lib/test/test_builtin.py:模块级与类级 sentinel 在所有 pickle 协议下均保持身份;而局部创建的 sentinel 序列化时会抛出pickle.PicklingError。
类型提示中的用法
sentinel 支持|(按位或)运算符,可用于类型表达式,实现"默认值可能是 int 或缺失状态"这样的联合类型标注:
MISSING = sentinel("MISSING") def next_value(default: int | MISSING = MISSING): ...官方文档与测试(Lib/test/test_builtin.py)均验证了以下行为:
missing | int与int | missing均生成合法的typing.Union;missing | missing结果仍为missing自身;missing | None生成(missing, NoneType)联合;- 与任意类型操作数(如
list[int]、int | str)组合均正常; - 与非法操作数(如
1)组合抛出TypeError。
该能力由 Objects/sentinelobject.c 中的数字协议钩子实现:
static PyNumberMethods sentinel_as_number = { .nb_or = _Py_union_type_or, };C API 层面的支持
针对嵌入与扩展场景,Doc/c-api/sentinel.rst 提供了三个接口(均于 Python 3.15 新增):
| C 接口 | 说明 |
|---|---|
PySentinel_Type | sentinel类型对象,与 Python 层的sentinel是同一对象 |
PySentinel_Check(o) | 判断o是否为 sentinel 对象或其子类;由于当前不支持子类化,该检查即精确匹配 |
PySentinel_CheckExact(o) | 判断o是否为 sentinel 对象(非子类);当前与PySentinel_Check等价 |
PySentinel_New(name, module_name, repr) | 创建新 sentinel,三个参数均为const char *;name不可为NULL,module_name与repr可为NULL,失败时返回NULL并设置异常 |
C 层创建函数 PySentinel_New 会逐个将 C 字符串转换为PyUnicode对象并构造实例;module_name传NULL时__module__被设为None,repr传NULL时repr()回退到name。
关于 pickle 的 C 层注意事项:module_name必须是可导入模块名,且 sentinel 需在该模块中以与name匹配的路径可访问,否则无法完成序列化还原。
源码实现要点速览
- 类型定义:
PySentinel_Type(Objects/sentinelobject.c),同时启用IMMUTABLETYPE与HAVE_GC; - 构造入口:
sentinel.__new__(clinic 生成,Objects/sentinelobject.c),name必须是str,repr必须是str或None; - 模块推断:
caller()(Objects/sentinelobject.c),基于当前帧的函数对象推导模块名; - 表示逻辑:
sentinel_repr(Objects/sentinelobject.c),repr字段优先、否则回退name; - 比较逻辑:复用
_Py_BaseObject_RichCompare,保证基于身份的比较语义; - GC 支持:
sentinel_traverse/sentinel_clear(Objects/sentinelobject.c)正确管理name、module、repr三个字段的引用。
兼容性与总结
本次变更聚焦两点:sentinel(name, *, repr=...)允许自定义表示;__module__变为可写。二者均为向后兼容的增量增强——原有调用方式(仅传name)不受影响,__name__依旧只读。
结合官方文档(Doc/library/functions.rst、Doc/whatsnew/3.15.rst)、C API 文档(Doc/c-api/sentinel.rst)与完整测试(Lib/test/test_builtin.py),可以确认 sentinel 已成为 Python 3.15 处理"缺失值"场景的推荐工具:唯一身份、is比较、真值语义、类型表达式参与、可 pickle,配合新增的repr=与可写__module__,无论用于标准库常量还是自定义库 API,都能写出语义清晰、调试友好的哨兵值。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考