在except子句中的断点无法访问绑定的异常。

9

考虑以下示例:

try:
    raise ValueError('test')
except ValueError as err:
    breakpoint()  # at this point in the debugger, name 'err' is not defined

这里,在输入breakpoint后,调试器无法访问绑定到err的异常实例:

$ python test.py 
--Return--
> test.py(4)<module>()->None
-> breakpoint()
(Pdb) p err
*** NameError: name 'err' is not defined

为什么会出现这种情况?我如何访问异常实例?目前我正在使用以下解决方法,但感觉很笨拙:
try:
    raise ValueError('test')
except ValueError as err:
    def _tmp():
        breakpoint()
    _tmp()
    # (lambda: breakpoint())()  # or this one alternatively

有趣的是,使用这个版本,当向调试器上移一帧时,我也可以访问绑定的异常err
$ python test.py 
--Return--
> test.py(5)_tmp()->None
-> breakpoint()
(Pdb) up
> test.py(6)<module>()
-> _tmp()
(Pdb) p err
ValueError('test')

通过dis反汇编

接下来我将比较两个版本,一个直接使用breakpoint,另一个把它封装在自定义函数_breakpoint中:

def _breakpoint():
    breakpoint()

try:
    raise ValueError('test')
except ValueError as err:
    breakpoint()   # version (a), cannot refer to 'err'
    # _breakpoint()  # version (b), can refer to 'err'
dis 的输出类似,除了一些内存位置和函数名称当然也不同:

diff

因此,必须是额外的堆栈帧使 pdb 能够引用绑定的异常实例。但是不清楚为什么会这样,因为在 except 块中任何东西都可以引用绑定的异常实例。

很棒的问题 :) - stonecharioteer
出于好奇,您使用的应用程序名称是什么,用于查找文本之间的差异? - JRodDynamite
@JRodDynamite 它叫做Meld - a_guest
3个回答

11

breakpoint()并不是在此函数被调用的确切位置停止执行的断点。相反,它是import pdb; pdb.set_trace()的简写,它将在下一行代码处停止执行(它在内部调用sys.settrace)。由于except块内没有更多的代码,因此执行将在退出该块后停止,因此名称err已被删除。通过在except块之后添加一行额外的代码可以更清楚地看到这一点:

try:
    raise ValueError('test')
except ValueError as err:
    breakpoint()
print()

得到以下结果:

$ python test.py 
> test.py(5)<module>()
-> print()
这意味着解释器即将执行第5行的print()语句,并已经执行了它之前的所有内容(包括删除名称err)。当使用另一个函数来包装breakpoint()时,解释器将在该函数的return事件处停止执行,因此except块尚未退出(并且err仍然可用):
$ python test.py 
--Return--
> test.py(5)<lambda>()->None
-> (lambda: breakpoint())()

如果在 breakpoint() 后面加上另外一个 pass 语句,也可以延迟退出 except 块:

try:
    raise ValueError('test')
except ValueError as err:
    breakpoint()
    pass

导致结果如下:

$ python test.py 
> test.py(5)<module>()
-> pass
(Pdb) p err
ValueError('test')
请注意,pass 必须放在单独的一行上,否则它将被跳过。
$ python test.py 
--Return--
> test.py(4)<module>()->None
-> breakpoint(); pass
(Pdb) p err
*** NameError: name 'err' is not defined

请注意--Return--,它意味着解释器已经到达了模块的末尾。


1
这就是答案。有趣。类比一下,就像在 IDE 中在一行上插入断点。那种心理模型非常合理。感谢 @a_guest :) 每天都会学到新东西。 - stonecharioteer

1

这是一个很好的问题!

当出现奇怪的情况时,我总是会对Python代码进行反汇编并查看字节码。

可以使用标准库中的dis模块来完成此操作。

但是,在代码中设置断点时,我无法对其进行反汇编 :-)

因此,我稍微修改了代码,并设置了一个标记变量abc = 10,以便在except语句之后显示发生了什么。

这是我的修改后的代码,我将其保存为main.py

try:
    raise ValueError('test')
except ValueError as err:
    abc = 10

当您反汇编代码时...
❯ python -m dis main.py 
  1           0 SETUP_FINALLY           12 (to 14)

  2           2 LOAD_NAME                0 (ValueError)
              4 LOAD_CONST               0 ('test')
              6 CALL_FUNCTION            1
              8 RAISE_VARARGS            1
             10 POP_BLOCK
             12 JUMP_FORWARD            38 (to 52)

  3     >>   14 DUP_TOP
             16 LOAD_NAME                0 (ValueError)
             18 COMPARE_OP              10 (exception match)
             20 POP_JUMP_IF_FALSE       50
             22 POP_TOP
             24 STORE_NAME               1 (err)
             26 POP_TOP
             28 SETUP_FINALLY            8 (to 38)

  4          30 LOAD_CONST               1 (10)
             32 STORE_NAME               2 (abc)
             34 POP_BLOCK
             36 BEGIN_FINALLY
        >>   38 LOAD_CONST               2 (None)
             40 STORE_NAME               1 (err)
             42 DELETE_NAME              1 (err)
             44 END_FINALLY
             46 POP_EXCEPT
             48 JUMP_FORWARD             2 (to 52)
        >>   50 END_FINALLY
        >>   52 LOAD_CONST               2 (None)
             54 RETURN_VALUE

你能感受到正在发生的事情。

你可以在优秀的文档或者Python模块每周网站上阅读更多关于dis模块的内容:

https://docs.python.org/3/library/dis.html https://docs.python.org/3/library/dis.html

当然,这不是一个完美的答案。实际上,我必须自己坐下来阅读文档。我很惊讶在处理except块中的变量abc之前调用了SETUP_FINALLY。此外,我不确定POP_TOP的效果-在存储err名称后立即执行。
附言:非常好的问题!我非常兴奋看到结果如何。

1
为什么在代码中包含breakpoint()时无法反汇编?它只是另一个函数调用(对我有效)。看到字节码很有趣,但我不确定它是否有助于回答问题。我比较了包含breakpoint()(lambda: breakpoint())()dis版本,它们很相似,除了调用函数的部分(例如,DELETE_NAME同时发生)。因此,相关差异(仅适用于pdb,因为在except块内,您显然可以访问绑定的异常)似乎是来自lambda的额外堆栈帧。 - a_guest
对我来说,这并不包含问题的答案。因此为-1。 - Jonathan Scholbach

-1

我如何访问异常实例?

嗯,这是很简单的。当使用breakpoint()时,我总是只需复制err变量;

try:
    raise ValueError('foo')
except Exception as err:
    e = err
    breakpoint()

它产生

PS C:\tmp> python .\test_exc.py
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 22:20:52) [MSC v.1916 32 bit (Intel)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.2.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: e
Out[1]: ValueError('foo')

(我正在使用IPython.embed()代替pdb.set_trace()作为我的PYTHONBREAKPOINT

为什么会这样呢?

也许查看try语句文档可以帮助解答。它说:

When an exception has been assigned using as target, it is cleared at the end of the except clause. This is as if

except E as N:
    foo 

was translated to

except E as N:
    try:
        foo
    finally:
        del N

This means the exception must be assigned to a different name to be able to refer to it after the except clause. Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.

现在显然,如果pdb.set_trace()(或IPython.embed())位于except块的最后一行,则会退出异常块并执行隐式的finally套件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接