【问题标题】:breakpoint in except clause doesn't have access to the bound exceptionexcept 子句中的断点无权访问绑定异常
【发布时间】:2020-10-28 23:17:02
【问题描述】:

考虑以下示例:

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 块内任何东西都可以引用绑定的异常实例。

【问题讨论】:

  • 好问题:)
  • 出于好奇,您用来查找文本之间差异的应用程序的名称是什么?
  • @JRodDynamite 它叫Meld

标签: python python-3.x exception breakpoints pdb


【解决方案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 statement documentation 会有所帮助。它说:

当一个异常被指定为目标时,它会在 except 子句的结尾。这好像

except E as N:
    foo 

被翻译成

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

这意味着必须将异常分配给不同的名称 能够在 except 子句之后引用它。异常被清除 因为带有附加到它们的回溯,它们形成了参考 与堆栈帧循环,使该帧中的所有本地人保持活动状态 直到下一次垃圾回收发生。

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

【讨论】:

    【解决方案2】:

    breakpoint() 不是断点,因为它会在此函数调用的确切位置停止执行。相反,它是import pdb; pdb.set_trace() 的简写,它将在next 行代码处停止执行(它在幕后调用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--,这意味着解释器已经到达模块的末尾。

    【讨论】:

    • 这就是答案。有趣的。一个类比是您如何在 IDE 中的一行上插入断点。这种心理模型很有意义。谢谢@a_guest :) 你每天都能学到新东西。
    【解决方案3】:

    这是一个很好的问题!

    当发生奇怪的事情时,我总是反汇编 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

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

    P.S.:很好的问题!我对结果如何感到非常兴奋。

    【讨论】:

    • 包含breakpoint()的代码为什么不能反汇编?这只是另一个函数调用(它对我有用)。看到字节码很有趣,但我不确定它是否有助于回答这个问题。我比较了包含breakpoint()(lambda: breakpoint())()dis 版本,除了调用函数的部分之外它们是相似的(例如DELETE_NAME 同时发生)。所以相关的区别(仅限pdb,因为在except 块内,您显然可以访问绑定异常),似乎是来自 lambda 的额外堆栈帧。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-17
    相关资源
    最近更新 更多