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
对。
pass
语句在 if
语句中。
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