PEP 626 – 用于调试和其他工具的精确行号。
- 作者:
- Mark Shannon <mark at hotpy.org>
- BDFL 委托:
- Pablo Galindo <pablogsal at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2020年7月15日
- Python 版本:
- 3.10
- 发布历史:
- 2020年7月17日
摘要
Python 应该保证在开启跟踪时,会为执行的*所有*代码行生成“行”跟踪事件,并且*只*为执行的代码行生成事件。
帧对象的 f_lineno 属性应始终包含预期的行号。在帧执行期间,预期的行号是当前正在执行的源代码的行号。帧完成后,无论是通过返回还是通过引发异常,预期的行号是最后执行的源代码行的行号。
确保正确行号的一个副作用是,一些字节码需要被标记为人工生成,并且没有有意义的行号。为了辅助工具,将添加一个新的 co_lines 属性,用于描述字节码到源代码的映射。
动机
sys.settrace 和相关工具的用户应该能够依赖于为所有代码行(且仅为实际代码)生成跟踪事件。他们还应该能够假定 f_lineno 中的行号是正确的。
当前的实现大部分做到了这一点,但在少数情况下会失败。这需要工具中的变通方法,并给其他 Python 实现带来了不便。
从长远来看,拥有此保证也有利于 CPython 的实现者,因为当前的行为不明显且存在一些奇怪的边缘情况。
基本原理
为了保证在预期时生成行事件,co_lnotab 属性(以其当前形式)不再是行号信息的真实来源。
与其尝试修复 co_lnotab 属性,不如添加一个新方法 co_lines(),它返回一个迭代器,遍历字节码偏移量和源代码行。
确保字节码正确标注以实现精确的行号信息意味着某些字节码必须标记为人工,并且没有行号。
必须小心,不要破坏现有工具。为了最大限度地减少破坏,co_lnotab 属性将被保留,但会根据需要延迟生成。
规范
行事件和 f_lineno 属性在*所有*情况下都应按有经验的 Python 用户预期的那样工作。
跟踪
跟踪会生成关于调用、返回、异常、执行的源代码行以及在某些情况下执行的指令的事件。
本 PEP 仅涵盖行事件。
当跟踪开启时,会在以下情况生成行事件:
- 到达新的源代码行。
- 发生向后跳转,即使它跳转到同一行,例如在列表推导中可能发生的情况。
此外,绝不会为未执行的源代码行生成行事件。
出于跟踪目的,什么是被认为是代码
所有表达式和表达式的一部分都被认为是可执行代码。
一般来说,所有语句也都被认为是可执行代码。但是,当一个语句跨越几行时,我们必须考虑语句的哪些部分被认为是可执行代码。
语句由关键字和表达式组成。并非所有关键字都具有直接的运行时效果,因此并非所有关键字都被视为可执行代码。例如,else 是 if 语句的必要部分,但 else 没有关联的运行时效果。
出于追踪目的,以下关键字将*不*被视为可执行代码
del– 待删除的表达式被视为可执行代码。else– 无运行时效果finally– 无运行时效果global– 纯粹的声明性nonlocal– 纯粹的声明性
所有其他关键字都被认为是可执行代码。
事件序列示例
在以下示例中,事件以“名称”、f_lineno 对的形式列出。
代码
1. global x
2. x = a
生成以下事件
"line" 2
代码
1. try:
2. pass
3. finally:
4. pass
生成以下事件
"line" 1
"line" 2
"line" 4
代码
1. for (
2. x) in [1]:
3. pass
4. return
生成以下事件
"line" 2 # evaluate [1]
"line" 1 # for
"line" 2 # store to x
"line" 3 # pass
"line" 1 # for
"line" 4 # return
"return" 1
f_lineno 属性
- 创建帧对象时,
f_lineno属性将被设置为函数或类定义的行;即def或class关键字所在的行。对于模块,它将被设置为零。 - 即使跟踪关闭且未生成事件,
f_lineno属性也将更新以匹配即将执行的行号。
代码对象的新 co_lines() 方法
co_lines() 方法将返回一个迭代器,它生成值元组,每个元组表示一系列字节码的行号。每个元组将由三个值组成
start– 字节码范围开始的偏移量(包含)end– 字节码范围结束的偏移量(不包含)line– 行号,如果给定范围内的字节码没有行号,则为None。
生成的序列将具有以下属性
- 序列中的第一个范围的
start将为0 (start, end)范围将是非递减且连续的。也就是说,对于任何一对元组,第二个元组的start将等于第一个元组的end。- 没有范围是向后的,也就是说对于所有三元组,
end >= start。 - 序列中的最后一个范围的
end将等于字节码的大小。 line将是正整数,或None
零宽度范围
允许零宽度范围,即 start == end 的范围。零宽度范围用于源代码中存在但已被字节码编译器消除的行。
co_linetable 属性
co_linetable 属性将包含行号信息。格式不透明、未指定,可能会在不通知的情况下更改。该属性仅为支持创建新的代码对象而公开。
co_lnotab 属性
历史上,co_lnotab 属性保存了字节码偏移量到行号的映射,但不支持没有行号的字节码。为了向后兼容,co_lnotab 字节对象将在需要时延迟创建。对于没有行号的字节码范围,将使用前一个字节码范围的行号。
解析 co_lnotab 表的工具应尽快迁移到使用新的 co_lines() 方法。
向后兼容性
co_lnotab 属性将在 3.10 中弃用,并在 3.12 中移除。
任何解析代码对象的 co_lnotab 属性的工具都需要在 3.12 发布之前迁移到使用 co_lines()。使用 sys.settrace 的工具将不受影响,除非它们收到的“行”事件更准确。
跟踪事件序列将改变的代码示例
在以下示例中,事件以“名称”、f_lineno 对的形式列出。
if 语句中的 pass 语句。
0. def spam(a):
1. if a:
2. eggs()
3. else:
4. pass
如果 a 为 True,则 Python 3.9 生成的事件序列为
"line" 1
"line" 2
"line" 4
"return" 4
从 3.10 开始,序列将是
"line" 1
"line" 2
"return" 2
多个 pass 语句。
0. def bar():
1. pass
2. pass
3. pass
Python 3.9 生成的事件序列是
"line" 3
"return" 3
从 3.10 开始,序列将是
"line" 1
"line" 2
"line" 3
"return" 3
C API
通过 C API 函数访问帧对象的 f_lineno 属性不变。f_lineno 可以通过 PyFrame_GetLineNumber 读取。f_lineno 只能通过 PyObject_SetAttr 和类似函数设置。
严禁直接通过底层数据结构访问 f_lineno。
进程外调试器和分析器
进程外工具,例如 py-spy [1],无法使用 C-API,必须自行解析行号表。尽管行号表格式可能会在没有警告的情况下更改,但除非绝对必要用于错误修复,否则在发布期间不会更改。
为了减少实现这些工具所需的工作,提供了以下 C 结构和实用函数。请注意,这些函数不属于 C-API,因此需要链接到任何需要使用它们的代码中。
typedef struct addressrange {
int ar_start;
int ar_end;
int ar_line;
struct _opaque opaque;
} PyCodeAddressRange;
void PyLineTable_InitAddressRange(char *linetable, Py_ssize_t length, int firstlineno, PyCodeAddressRange *range);
int PyLineTable_NextAddressRange(PyCodeAddressRange *range);
int PyLineTable_PreviousAddressRange(PyCodeAddressRange *range);
PyLineTable_InitAddressRange 从行号表和第一行号初始化 PyCodeAddressRange 结构体。
PyLineTable_NextAddressRange 将范围推进到下一个条目,如果有效则返回非零值。
PyLineTable_PreviousAddressRange 将范围回退到前一个条目,如果有效则返回非零值。
注意
linetable 中的数据是不可变的,但其生命周期取决于其代码对象。为了可靠操作,在调用 PyLineTable_InitAddressRange 之前,应将 linetable 复制到本地缓冲区中。
尽管这些函数不属于 C-API,但它们将由 CPython 的所有未来版本提供。PyLineTable_ 函数不调用 C-API,因此可以安全地复制到任何需要使用它们的工具中。PyCodeAddressRange 结构不会改变,但 _opaque 结构不属于规范的一部分,可能会改变。
注意
PyCodeAddressRange 结构已从本 PEP 的原始版本更改,其中添加的字段已定义,但可能会更改。
例如,以下代码打印出所有地址范围
void print_address_ranges(char *linetable, Py_ssize_t length, int firstlineno)
{
PyCodeAddressRange range;
PyLineTable_InitAddressRange(linetable, length, firstlineno, &range);
while (PyLineTable_NextAddressRange(&range)) {
printf("Bytecodes from %d (inclusive) to %d (exclusive) ",
range.start, range.end);
if (range.line < 0) {
/* line < 0 means no line number */
printf("have no line number\n");
}
else {
printf("have line number %d\n", range.line);
}
}
}
性能影响
一般来说,性能应该没有变化。进行跟踪时,程序应该运行得更快一些,因为新的表格式可以在设计时考虑到行号计算速度。包含长序列 pass 语句的代码可能会变慢一些。
参考实现
https://github.com/markshannon/cpython/tree/new-linetable-format-version-2
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
参考资料
来源:https://github.com/python/peps/blob/main/peps/pep-0626.rst
最后修改时间:2025-02-01 08:55:40 GMT