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

breakpoint in except clause doesn't have access to the bound exception

考虑以下示例:

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 的输出是相似的,除了一些内存位置和当然函数的名称:

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

这是一个很好的问题!

当发生一些奇怪的事情时,我总是会解开assemble Python 代码并查看字节码。

这可以通过标准库中的 dis 模块来完成。

这里有一个问题,当代码中有断点时,我无法取消-assemble :-)

所以,我稍微修改了代码,并设置了一个标记变量 abc = 10 以使 except 语句之后发生的事情可见。

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

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

然后当您取消 assemble 代码时...

❯ 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

当然,这不是一个完美的答案。实际上,我必须自己坐下来阅读文档。我很惊讶 SETUP_FINALLY 在处理 except 块中的变量 abc 之前被调用。另外,我不确定 POP_TOP 的效果是什么 - 在存储 err 名称后立即执行。

P.S.: 好问题!结果如何,我非常兴奋。

breakpoint() is not a breakpoint in the sense that it halts execution at the exact location of this function call. Instead it's a shorthand for import pdb; pdb.set_trace() which will halt execution at the next line of code (it calls 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())()

except 块的退出也可以通过在 breakpoint():

之后添加一个额外的 pass 语句来延迟
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-- 这意味着解释器已经到达模块的末尾。

如何访问异常实例?

好吧,这是简单的部分。使用 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 statement documentation 会有帮助。它说:

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())位于异常块的最后一行,它将退出异常块并执行隐式 finally 套件.