简体   繁体   English

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

[英]How to debug the stack trace that causes a subsequent exception in python?

Python (and ipython) has very powerful post-mortem debugging capabilities, allowing variable inspection and command execution at each scope in the traceback. Python(和ipython)具有非常强大的事后调试能力,允许在traceback中的每个scope上进行变量检查和命令执行。 The up/down debugger commands allow changing frame for the stack trace of the final exception, but what about the __cause__ of that exception, as defined by the raise... from... syntax? up/down 调试器命令允许更改最终异常的堆栈跟踪的帧,但是该异常的__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

Is there a way to access bab from the debugger?有没有办法从调试器访问bab

EDIT: I realized post-mortem debugging isn't just a feature of ipython and ipdb, it's actually part of vanilla pdb.编辑:我意识到事后调试不仅仅是 ipython 和 ipdb 的一个特性,它实际上是 vanilla pdb 的一部分。 The above can also be reproduced by putting the code into a script testerr.py and running python -m pdb testerr.py and running continue .也可以通过将代码放入脚本testerr.py并运行python -m pdb testerr.py并运行continue来重现上述内容。 After the error, it says错误后,它说

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

and gives a debugger at the same spot.并在同一地点提供调试器。

You can use the with_traceback(tb) method to preserve the original exception's traceback:您可以使用with_traceback(tb)方法来保留原始异常的回溯:

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

Note that I have updated the code to raise an exception instance rather than the exception class.请注意,我已更新代码以引发异常实例,而不是异常 class。

Here is the full code snippet in iPython :这是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 - An alternative inferior approach编辑 - 另一种劣质方法

Addressing @user2357112supportsMonica's first comment , if you wish to avoid multiple dumps of the original exception's traceback in the log, it's possible to raise from None .解决@user2357112supportsMonica 的第一条评论,如果您希望避免在日志中多次转储原始异常的回溯,可以raise from None However, as @user2357112supportsMonica's second comment states, this hides the original exception's message.但是,正如@user2357112supportsMonica 的第二条评论所述,这隐藏了原始异常的消息。 This is particularly problematic in the common case where you're not post-mortem debugging but rather inspecting a printed traceback.这在您不是事后调试而是检查打印的回溯的常见情况下尤其成问题。

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

Here is the code snippet in iPython :这是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

Raising from None is required since otherwise the chaining would be done implicitly , attaching the original exception as the new exception's __context__ attribute.需要from None引发,否则链接将被隐式完成,将原始异常附加为新异常的__context__属性。 Note that this differs from the __cause__ attribute which is set when the chaining is done explicitly.请注意,这与显式完成链接时设置的__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 works and should be your go-to procedure, but if the trace is a bit harder to debug, you may instead use the trace module. Yoel 答案有效,应该是您的首选程序,但如果跟踪有点难以调试,您可以改用trace模块。

The trace module will print out each instruction executed, line by line.跟踪模块将逐行打印出执行的每条指令。 There is a catch, though.不过,有一个问题。 Standard library and package calls will also be traced, and this likely means that the trace will be flooded with code that is not meaningful.标准库和 package 调用也将被跟踪,这可能意味着跟踪将被无意义的代码淹没。

To avoid this behavior, you may pass the --ignore-dir argument with the location of your Python library and site packages folder.为避免此行为,您可以将--ignore-dir参数与您的 Python 库和站点包文件夹的位置一起传递。

Run python -m site to find the locations of your site packages, then call trace with the following arguments:运行python -m site以查找站点包的位置,然后使用以下 arguments 调用跟踪:

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

Replacing the ignore-dir with all folders and the main.py args with a script location and arguments.ignore-dir替换为所有文件夹,将main.py args替换为脚本位置和 arguments。

You may also use the Trace module directly in your code if you want to run a certain function, refer to this example extracted from https://docs.python.org/3.0/library/trace.html : You may also use the Trace module directly in your code if you want to run a certain function, refer to this example extracted from 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")

I also just found a way to do this without modifying the underlying source code - simply running commands in the post-mortem debugger.我还刚刚找到了一种无需修改底层源代码的方法——只需在事后调试器中运行命令。

I saw from this answer you can get the locals directly from the traceback instance.我从这个答案中看到,您可以直接从回溯实例中获取本地人。

(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

Because sys.last_value.__traceback__ is sys.last_traceback , and ipdb makes use of the latter, you can simply move along a chain of Exceptions, and debug at the desired level by overwriting it.因为sys.last_value.__traceback__ is sys.last_traceback ,而ipdb使用后者,所以您可以简单地沿着异常链移动,并通过覆盖它来调试所需的级别。 Starting at sys.last_value , walk up the val.__context__ chain to the desired level ( new ), then set sys.last_traceback = new.__traceback__ , and invoke %debug .sys.last_value开始,将val.__context__链向上移动到所需的级别( new ),然后设置sys.last_traceback = new.__traceback__ ,并调用%debug

I wrote a small IPython magic, %chain , to make it easy to inspect the exception chain, move to an arbitrary or relative depth, or to the end of the chain, and debug.我写了一个小的 IPython 魔法, %chain ,以便于检查异常链,移动到任意或相对深度,或者到链的末端,并进行调试。 Just drop it in your iPython startup directory (eg ~/.ipython/profile_default/startup , and %chain -h for usage.)只需将它放在您的 iPython 启动目录中(例如~/.ipython/profile_default/startup%chain -h以供使用。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM