如何调试导致python中后续异常的堆栈跟踪



Python(和ipython(具有非常强大的死后调试功能,允许在回溯中的每个作用域执行变量检查和命令。向上/向下调试器命令允许更改最终异常的堆栈跟踪的帧,但该异常的__cause__(由raise ... from ...语法定义(呢?

Python 3.7.6 (default, Jan  8 2020, 13:42:34) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.11.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]: def foo(): 
...:     bab = 42 
...:     raise TypeError 
...:                                                                                                                                      
In [2]: try: 
...:     foo() 
...: except TypeError as err: 
...:     barz = 5 
...:     raise ValueError from err 
...:                                                                                                                                      
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-dd046d7cece0> in <module>
1 try:
----> 2     foo()
3 except TypeError as err:
<ipython-input-1-da9a05838c59> in foo()
2     bab = 42
----> 3     raise TypeError
4 
TypeError: 
The above exception was the direct cause of the following exception:
ValueError                                Traceback (most recent call last)
<ipython-input-2-dd046d7cece0> in <module>
3 except TypeError as err:
4     barz = 5
----> 5     raise ValueError from err
6 
ValueError: 
In [3]: %debug                                                                                                                               
> <ipython-input-2-dd046d7cece0>(5)<module>()
2     foo()
3 except TypeError as err:
4     barz = 5
----> 5     raise ValueError from err
6 
ipdb> barz                                                                                                                                   
5
ipdb> bab                                                                                                                                    
*** NameError: name 'bab' is not defined
ipdb> down                                                                                                                                   
*** Newest frame
ipdb> up                                                                                                                                     
*** Oldest frame

有没有办法从调试器访问bab

编辑:我意识到死后调试不仅仅是ipython和ipdb的一个功能,它实际上是普通pdb的一部分。还可以通过将代码放入脚本testerr.py并运行python -m pdb testerr.pycontinue来再现上述内容。错误发生后,上面写着

Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program

并在同一位置给出调试器。

您可以使用with_traceback(tb)方法来保留原始异常的回溯:

try: 
foo()
except TypeError as err:
barz = 5
raise ValueError().with_traceback(err.__traceback__) from err

请注意,我已经更新了代码以引发异常实例,而不是异常类。

以下是iPython:中的完整代码片段

In [1]: def foo(): 
...:     bab = 42 
...:     raise TypeError() 
...:                                                                                                                                                         
In [2]: try: 
...:     foo() 
...: except TypeError as err: 
...:     barz = 5 
...:     raise ValueError().with_traceback(err.__traceback__) from err 
...:                                                                                                                                                         
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-a5a6d81e4c1a> in <module>
1 try:
----> 2     foo()
3 except TypeError as err:
<ipython-input-1-ca1efd1bee60> in foo()
2     bab = 42
----> 3     raise TypeError()
4 
TypeError: 
The above exception was the direct cause of the following exception:
ValueError                                Traceback (most recent call last)
<ipython-input-2-a5a6d81e4c1a> in <module>
3 except TypeError as err:
4     barz = 5
----> 5     raise ValueError().with_traceback(err.__traceback__) from err
6 
<ipython-input-2-a5a6d81e4c1a> in <module>
1 try:
----> 2     foo()
3 except TypeError as err:
4     barz = 5
5     raise ValueError().with_traceback(err.__traceback__) from err
<ipython-input-1-ca1efd1bee60> in foo()
1 def foo():
2     bab = 42
----> 3     raise TypeError()
4 
ValueError: 
In [3]: %debug                                                                                                                                                  
> <ipython-input-1-ca1efd1bee60>(3)foo()
1 def foo():
2     bab = 42
----> 3     raise TypeError()
4 
ipdb> bab                                                                                                                                                       
42
ipdb> u                                                                                                                                                         
> <ipython-input-2-a5a6d81e4c1a>(2)<module>()
1 try:
----> 2     foo()
3 except TypeError as err:
4     barz = 5
5     raise ValueError().with_traceback(err.__traceback__) from err
ipdb> u                                                                                                                                                         
> <ipython-input-2-a5a6d81e4c1a>(5)<module>()
2     foo()
3 except TypeError as err:
4     barz = 5
----> 5     raise ValueError().with_traceback(err.__traceback__) from err
6 
ipdb> barz                                                                                                                                                      
5

EDIT-另一种劣质方法

针对@user2357112supportsMonica的第一条注释,如果您希望避免在日志中多次转储原始异常的回溯,可以使用raise from None。然而,正如@user2357112supportsMonica的第二条评论所说,这隐藏了原始异常的消息。在通常情况下,这尤其有问题,因为您不是在事后调试,而是在检查打印的回溯。

try: 
foo()
except TypeError as err:
barz = 5
raise ValueError().with_traceback(err.__traceback__) from None

以下是iPython:中的代码片段

In [4]: try: 
...:     foo() 
...: except TypeError as err: 
...:     barz = 5 
...:     raise ValueError().with_traceback(err.__traceback__) from None    
...:                                                                                                                                                         
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-6-b090fb9c510e> in <module>
3 except TypeError as err:
4     barz = 5
----> 5     raise ValueError().with_traceback(err.__traceback__) from None
6 
<ipython-input-6-b090fb9c510e> in <module>
1 try:
----> 2     foo()
3 except TypeError as err:
4     barz = 5
5     raise ValueError().with_traceback(err.__traceback__) from None
<ipython-input-2-ca1efd1bee60> in foo()
1 def foo():
2     bab = 42
----> 3     raise TypeError()
4 
ValueError: 
In [5]: %debug                                                                                                                                                  
> <ipython-input-2-ca1efd1bee60>(3)foo()
1 def foo():
2     bab = 42
----> 3     raise TypeError()
4 
ipdb> bab                                                                                                                                                       
42
ipdb> u                                                                                                                                                         
> <ipython-input-6-b090fb9c510e>(2)<module>()
1 try:
----> 2     foo()
3 except TypeError as err:
4     barz = 5
5     raise ValueError().with_traceback(err.__traceback__) from None
ipdb> u                                                                                                                                                         
> <ipython-input-6-b090fb9c510e>(5)<module>()
3 except TypeError as err:
4     barz = 5
----> 5     raise ValueError().with_traceback(err.__traceback__) from None
6 
ipdb> barz                                                                                                                                                      
5

提升from None是必需的,因为否则链接将隐式完成,将原始异常附加为新异常的__context__属性。请注意,这与__cause__属性不同,后者是在显式执行链接时设置的。

In [6]: try: 
...:     foo() 
...: except TypeError as err: 
...:     barz = 5 
...:     raise ValueError().with_traceback(err.__traceback__) 
...:                                                                                                                                                         
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-ee78991171cb> in <module>
1 try:
----> 2     foo()
3 except TypeError as err:
<ipython-input-2-ca1efd1bee60> in foo()
2     bab = 42
----> 3     raise TypeError()
4 
TypeError: 
During handling of the above exception, another exception occurred:
ValueError                                Traceback (most recent call last)
<ipython-input-5-ee78991171cb> in <module>
3 except TypeError as err:
4     barz = 5
----> 5     raise ValueError().with_traceback(err.__traceback__)
6 
<ipython-input-5-ee78991171cb> in <module>
1 try:
----> 2     foo()
3 except TypeError as err:
4     barz = 5
5     raise ValueError().with_traceback(err.__traceback__)
<ipython-input-2-ca1efd1bee60> in foo()
1 def foo():
2     bab = 42
----> 3     raise TypeError()
4 
ValueError: 

Yoel answer有效,应该是您的首选过程,但如果跟踪有点难以调试,您可以使用trace模块。

跟踪模块将逐行打印出执行的每条指令。不过,有一个陷阱。标准库和包调用也将被跟踪,这可能意味着跟踪中会充斥着没有意义的代码。

为了避免这种行为,您可以传递带有Python库和站点包文件夹位置的--ignore-dir参数。

运行python -m site查找站点包的位置,然后使用以下参数调用trace:

python -m trace --trace --ignore-dir=/usr/lib/python3.8:/usr/local/lib/python3.8/dist-packages main.py args

用所有文件夹替换ignore-dir,用脚本位置和参数替换main.py args

如果您想运行某个函数,也可以直接在代码中使用Trace模块,请参阅从https://docs.python.org/3.0/library/trace.html:

import sys
import trace
# create a Trace object, telling it what to ignore, and whether to
# do tracing or line-counting or both.
tracer = trace.Trace(
ignoredirs=[sys.prefix, sys.exec_prefix],
trace=0,
count=1)
# run the new command using the given tracer
tracer.run('main()')
# make a report, placing output in /tmp
r = tracer.results()
r.write_results(show_missing=True, coverdir="/tmp")

我还刚刚找到了一种不用修改底层源代码就能做到这一点的方法——只需在死后调试器中运行命令。

我从这个答案中看到,您可以直接从回溯实例中获取本地人。

(Pdb) ll
1  -> def foo():
2         bab = 42
3         raise TypeError
4   
5     try:
6         foo()
7     except TypeError as err:
8         barz = 5
9  >>     raise ValueError from err
10
(Pdb) err # not sure why err is not defined
*** NameError: name 'err' is not defined
(Pdb) import sys
(Pdb) sys.exc_info()
(<class 'AttributeError'>, AttributeError("'Pdb' object has no attribute 'do_sys'"), <traceback object at 0x107cb5be0>)
(Pdb) err = sys.exc_info()[1].__context__
(Pdb) err # here we go
ValueError()
(Pdb) err.__cause__
TypeError()
(Pdb) err.__traceback__.tb_next.tb_next.tb_next.tb_next.tb_frame.f_locals['barz']
5
(Pdb) err.__cause__.__traceback__.tb_next.tb_frame.f_locals['bab']
42

因为sys.last_value.__traceback__ is sys.last_tracebackipdb使用后者,所以您可以简单地沿着Exceptions链移动,并通过覆盖它在所需级别进行调试。从sys.last_value开始,沿着val.__context__链向上走到所需级别(new(,然后设置sys.last_traceback = new.__traceback__,并调用%debug

我写了一个小的IPython魔术,%chain,让它可以很容易地检查异常链,移动到任意或相对的深度,或者移动到链的末尾,并进行调试。只需将其放在iPython启动目录中即可(例如~/.ipython/profile_default/startup%chain -h(

相关内容

  • 没有找到相关文章

最新更新