为什么breakpoint()在for循环中不提示幂等定位?

Why breakpoint() doesn't prompt idempotent location in for loop?

我有一个非常简单的测试文件breakpoint_test.py

for i in range(3):                       #1
    breakpoint()                         #2
    print(f"first print in loop {i}")    #3
    breakpoint()                         #4
    print(f"second print in loop {i}")   #5

这是我 运行 并按住 c 继续 .

时的输出
(py3) nakita@machine:~/tmp $ python breakpoint_test.py 
> /Users/nakita/tmp/breakpoint_test.py(3)<module>()
-> print(f"first print in loop {i}")                    <---------------looks good here!
(Pdb) c
first print in loop 0
> /Users/nakita/tmp/breakpoint_test.py(5)<module>()
-> print(f"second print in loop {i}")
(Pdb) c
second print in loop 0


> /Users/nakita/tmp/breakpoint_test.py(2)<module>()
-> breakpoint()                                         <---------------weird here!
(Pdb) c
first print in loop 1
> /Users/nakita/tmp/breakpoint_test.py(5)<module>()
-> print(f"second print in circle {i}")
(Pdb) c
second print in loop 1


> /Users/nakita/tmp/breakpoint_test.py(2)<module>()
-> breakpoint()
(Pdb) c
first print in loop 2
> /Users/nakita/tmp/breakpoint_test.py(5)<module>()
-> print(f"second print in circle {i}")
(Pdb) c
second print in loop 2

我希望 breakpoint() 会提示将在 breakpoint() 之后立即执行的行。当代码第一次进入 for 循环时就是如此。但是在第二个循环中迭代时,第一个断点 () 提示 breakpoint() 行本身而不是 -> print(f"first print in loop {i}")。但是,循环体中的第二个 breakpoint() 按我的预期工作。看起来行为是第一个breakpoint()在for循环体中会被牺牲掉。有人知道为什么吗?

我在 python 3.7.3 和 3.8.0 上测试过。 我已阅读 PEP 553 -- Built-in breakpoint()

Python 试图在下一行的开头开始调试,但它的 "next line" 检测有点不稳定。


默认情况下,breakpoint 调用 pdb.set_trace,它设置将在下一个跟踪事件上执行的 trace function。本例中的下一个跟踪事件是 'line' 事件,当 Python 认为执行已进入新行时触发。

在循环中第一个 breakpoint 的第一次执行以及第二个 breakpoint 的所有执行中,下一个 'line' 事件在下一个的第一个操作码上触发线。但是,在第二次和以后执行第一次 breakpoint 时,会发生一些不同的事情。

Python 通过检查当前字节码指令索引是否对应于一行的第一条指令,或者在执行的最后一条指令之前的索引处的指令来确定新的源代码行已经开始。您可以在 Python/ceval.c.

中的 maybe_call_line_trace 中看到

Python 仅更新 instr_prev,用于确定执行的最后一条指令的变量,当跟踪处于活动状态时。当您点击 c 继续执行时,跟踪功能将被停用。 (如果您使用 PDB break 命令设置了任何断点,它将保持活动状态,因为跟踪函数需要处理这些断点,但 breakpoint() 调用不会通过该机制。)

在"non-weird"断点处,Python在"first instruction of a line"条件下,在下一行的第一个操作码上触发下一行事件。

在 "weird" 断点上,instr_prev 仍然具有您上次点击 c 时的值,因为在该点禁用了跟踪。该值用于比当前行晚的行,因此 Python 在 "instruction at an index prior to the last instruction executed" 条件下触发 breakpoint() 行的下一个操作码上的下一行事件。 (breakpoint() 行的下一个操作码是 POP_TOP,用于清除 breakpoint 的 return 值。)如果 Python 一直保持更好的跟踪最后执行的指令,它不会触发该事件。