摘要
本PEP为BaseException添加了一个可选的__timestamp_ns__属性,用于记录异常实例化的时间,且没有额外性能开销。当通过环境变量或命令行标志启用时,格式化的异常回溯会显示该时间戳。
动机
随着异常组(PEP 654)的引入,Python程序可以同时传播多个不相关的异常。在调试这些异常,或将异常与外部日志和指标关联时,了解每个异常发生的时间通常与了解发生了什么同样重要。
考虑一个异步服务,它同时从多个后端获取数据并报告所有错误。结果的ExceptionGroup包含提交顺序的所有错误,但没有每个异常发生的时间指示。启用PYTHON_TRACEBACK_TIMESTAMPS=iso后,输出会为每个异常显示时间戳,便于分析发生顺序。
规范
异常时间戳属性
向BaseException添加一个新的读写属性__timestamp_ns__。它以C int64_t类型存储自Unix纪元以来的纳秒数(UTC)。当禁用时间戳、用于控制流异常或读取时钟失败时,该值为0。
复用的异常实例(如MemoryError)在分发时记录时间戳,而非原始分配时。
控制流异常
为避免影响正常控制流性能,即使功能启用,也不为StopIteration或StopAsyncIteration收集时间戳。这些异常在迭代过程中极高频触发。
配置
通过CPython的标准机制启用:
PYTHON_TRACEBACK_TIMESTAMPS环境变量:设置为ns或1表示纳秒精度小数时间戳,iso表示ISO 8601 UTC格式-X traceback_timestamps=<format>命令行选项
显示格式
时间戳附加到异常回溯中的异常消息行,格式为<@timestamp>。仅影响格式化的回溯输出;str(exc)和repr(exc)不变。
回溯模块更新
TracebackException和公共格式化函数增加了timestamps关键字参数(默认为None),提供None(遵循全局配置)、False(从不显示)、True(显示任何非零值)三种行为。
提供新的工具函数traceback.strip_exc_timestamps(text),用于从格式化的回溯字符串中去除<@...>时间戳后缀。
Doctest更新
添加新的doctest.IGNORE_EXCEPTION_TIMESTAMPS选项标志,使doctest在比较前去除实际输出中的时间戳。
原理
时间戳存储为BaseException C结构中的单个int64_t字段。与使用异常备注(PEP 678)相比,结构字段在不填充时零成本,避免在抛出时创建字符串和列表对象,将所有格式化工作推迟到回溯渲染时。
实例化时间 vs. 抛出时间:在普遍常见的raise SomeError(...)形式中,两者相同。当不同时,实例化时间通常更有用:重新抛出保留了错误首次发生的时间。
性能测量:使用pyperformance套件测试,未观察到显著性能变化。仅对StopIteration/StopAsyncIteration的特殊处理在async_generators基准测试中显示约10%的性能提升。
向后兼容性
该功能默认禁用,不影响现有异常处理代码。__timestamp_ns__属性始终可读,未收集时返回0。
当禁用时间戳时,异常以传统2元组格式(type, args)pickle。存在非零时间戳时,异常pickle为3元组格式(type, args, state_dict),其中__timestamp_ns__在状态字典中。
安全影响
无。功能为选择加入且默认禁用。
如何教学
__timestamp_ns__属性和配置选项将记录在异常模块参考、回溯模块参考和命令行界面文档中。这是一个高级功能,默认禁用,除非显式启用,否则不可见。不需要在入门材料中介绍。
参考实现
CPython PR #129337。
被拒绝的想法
- 使用异常备注:需要在抛出时创建字符串和列表对象,即使在未显示时间戳时也产生开销
- 使用sys.excepthook:仅在未捕获异常到达顶层时运行,子异常都会获得相同的时间戳
- 使用sys.monitoring:比结构字段方法成本更高,RAISE事件每帧触发一次而不是每个异常触发一次
- 运行时API:这是一个操作员级别的设置,应由启动进程的人配置,而非库或应用程序代码
- 自定义时间戳格式:会增加显著复杂性
待解决问题
- 显示位置的最终确定(附加到
Type: message行 vs. 回溯头行) - sys.monitoring替代方案的基准测试
- 记录首次抛出时间的可行性
致谢
感谢 Nathaniel J. Smith 提出原始想法,感谢 Daniel Colascione 在2025年对实现提供的初步审阅反馈。FINISHED