如何调试导致 python 中后续异常的堆栈跟踪?
How to debug the stack trace that causes a subsequent exception in python?
Python(和ipython)具有非常强大的post-mortem 调试功能,允许在回溯中的每个范围内进行变量检查和命令执行。 up/down 调试器命令允许更改最终异常堆栈跟踪的帧,但是 raise ... from ...
语法定义的那个异常的 __cause__
呢?
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
?
编辑:我意识到 post-mortem 调试不仅仅是 ipython 和 ipdb 的一个特性,它实际上是 vanilla pdb 的一部分。也可以通过将代码放入脚本 testerr.py
和 运行 python -m pdb testerr.py
和 运行 continue
来重现上述内容。报错后显示
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
并在同一位置提供调试器。
您可以使用 with_traceback(tb)
method 来保留原始异常的回溯:
try:
foo()
except TypeError as err:
barz = 5
raise ValueError().with_traceback(err.__traceback__) from err
请注意,我已更新代码以引发异常实例而不是异常 class。
这是 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
编辑 - 另一种较差的方法
寻址 @user2357112supportsMonica 的 , if you wish to avoid multiple dumps of the original exception's traceback in the log, it's possible to raise from None
. However, as @user2357112supportsMonica's 状态,这隐藏了原始异常的消息。在您不是 post-mortem 调试而是检查打印的回溯的常见情况下,这尤其成问题。
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
,否则链接将完成 implicitly,将原始异常附加为新异常的 __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 答案有效,应该是您的首选程序,但如果跟踪难以调试,您可以改用 trace
模块。
跟踪模块将逐行打印出执行的每条指令。不过有一个问题。还将跟踪标准库和程序包调用,这可能意味着跟踪将充斥着无意义的代码。
为避免此行为,您可以将 --ignore-dir
参数与 Python 库和站点包文件夹的位置一起传递。
运行 python -m site
找到您的站点包的位置,然后使用以下参数调用跟踪:
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")
我还找到了一种无需修改底层源代码即可执行此操作的方法 - 只需 post-mortem 调试器中的 运行 命令即可。
我从 看到你可以直接从回溯实例中获取局部变量。
(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
我很惊讶没有简单的方法来沿着异常链移动,并在所需级别进行调试。所以我写了一个小的 IPython 魔法 %chain,让它变得简单。只需将其放入您的 iPython 启动目录(例如 ~/.ipython/profile_default/startup
)
Python(和ipython)具有非常强大的post-mortem 调试功能,允许在回溯中的每个范围内进行变量检查和命令执行。 up/down 调试器命令允许更改最终异常堆栈跟踪的帧,但是 raise ... from ...
语法定义的那个异常的 __cause__
呢?
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
?
编辑:我意识到 post-mortem 调试不仅仅是 ipython 和 ipdb 的一个特性,它实际上是 vanilla pdb 的一部分。也可以通过将代码放入脚本 testerr.py
和 运行 python -m pdb testerr.py
和 运行 continue
来重现上述内容。报错后显示
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
并在同一位置提供调试器。
您可以使用 with_traceback(tb)
method 来保留原始异常的回溯:
try:
foo()
except TypeError as err:
barz = 5
raise ValueError().with_traceback(err.__traceback__) from err
请注意,我已更新代码以引发异常实例而不是异常 class。
这是 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
编辑 - 另一种较差的方法
寻址 @user2357112supportsMonica 的 raise from None
. However, as @user2357112supportsMonica's
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
,否则链接将完成 implicitly,将原始异常附加为新异常的 __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 答案有效,应该是您的首选程序,但如果跟踪难以调试,您可以改用 trace
模块。
跟踪模块将逐行打印出执行的每条指令。不过有一个问题。还将跟踪标准库和程序包调用,这可能意味着跟踪将充斥着无意义的代码。
为避免此行为,您可以将 --ignore-dir
参数与 Python 库和站点包文件夹的位置一起传递。
运行 python -m site
找到您的站点包的位置,然后使用以下参数调用跟踪:
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")
我还找到了一种无需修改底层源代码即可执行此操作的方法 - 只需 post-mortem 调试器中的 运行 命令即可。
我从
(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
我很惊讶没有简单的方法来沿着异常链移动,并在所需级别进行调试。所以我写了一个小的 IPython 魔法 %chain,让它变得简单。只需将其放入您的 iPython 启动目录(例如 ~/.ipython/profile_default/startup
)