27

我正在开发一个 Python 库,第三方开发人员使用它来为我们的核心应用程序编写扩展。

我想知道是否可以在引发异常时修改回溯,因此最后一个堆栈帧是对开发人员代码中库函数的调用,而不是库中引发异常的行。堆栈底部还有一些框架包含对首次加载代码时使用的函数的引用,理想情况下我也希望将其删除。

提前感谢您的任何建议!

4

7 回答 7

14

您可以通过使用回溯的 tb_next 元素轻松删除回溯的顶部:

except:
    ei = sys.exc_info()
    raise ei[0], ei[1], ei[2].tb_next

tb_next 是一个只读属性,所以我不知道从底部删除东西的方法。您也许可以使用属性机制来允许访问该属性,但我不知道该怎么做。

于 2012-12-16T05:50:38.673 回答
12

看看 jinja2 在这里做了什么:

https://github.com/mitsuhiko/jinja2/blob/5b498453b5898257b2287f14ef6c363799f1405a/jinja2/debug.py

这很丑陋,但它似乎做了你需要做的事情。我不会在这里复制粘贴示例,因为它很长。

于 2009-10-21T22:13:23.860 回答
6

从 Python 3.7 开始,您可以实例化一个新traceback对象并.with_traceback()在抛出时使用该方法。下面是一些演示代码,使用其中一个sys._getframe(1)(或更强大的替代方案)会引发一段AssertionError时间,使您的调试器相信错误发生在myassert(False)sys._getframe(1)省略顶部堆栈帧。

我应该补充的是,虽然这在调试器中看起来不错,但控制台行为揭示了它的实际作用:

Traceback (most recent call last):
  File ".\test.py", line 35, in <module>
    myassert_false()
  File ".\test.py", line 31, in myassert_false
    myassert(False)
  File ".\test.py", line 26, in myassert
    raise AssertionError().with_traceback(back_tb)
  File ".\test.py", line 31, in myassert_false
    myassert(False)
AssertionError

我没有删除堆栈的顶部,而是添加了倒数第二帧的副本。

无论如何,我专注于调试器的行为方式,似乎这个工作正常:

"""Modify traceback on exception.

See also https://github.com/python/cpython/commit/e46a8a
"""

import sys
import types


def myassert(condition):
    """Throw AssertionError with modified traceback if condition is False."""
    if condition:
        return

    # This function ... is not guaranteed to exist in all implementations of Python.
    # https://docs.python.org/3/library/sys.html#sys._getframe
    # back_frame = sys._getframe(1)
    try:
        raise AssertionError
    except AssertionError:
        traceback = sys.exc_info()[2]
        back_frame = traceback.tb_frame.f_back

    back_tb = types.TracebackType(tb_next=None,
                                  tb_frame=back_frame,
                                  tb_lasti=back_frame.f_lasti,
                                  tb_lineno=back_frame.f_lineno)
    raise AssertionError().with_traceback(back_tb)


def myassert_false():
    """Test myassert(). Debugger should point at the next line."""
    myassert(False)


if __name__ == "__main__":
    myassert_false()

在此处输入图像描述

于 2019-11-12T15:15:53.970 回答
2

不更改回溯怎么办?您要求的两件事都可以通过不同的方式更轻松地完成。

  1. 如果库中的异常在开发人员的代码中被捕获并引发了新的异常,那么原始的回溯当然会被丢弃。这就是通常处理异常的方式......如果您只允许引发原始异常但您将其删除以删除所有“上部”框架,那么实际异常将没有意义,因为回溯中的最后一行不会本身能够引发异常。
  2. 要删除最后几帧,您可以请求缩短回溯...诸如 traceback.print_exception() 之类的内容采用“限制”参数,您可以使用该参数跳过最后几个条目。

也就是说,如果你真的需要的话,应该很有可能处理回溯......但是你会在哪里做呢?如果在最顶层的一些包装器代码中,那么您可以简单地获取回溯,取出切片以删除您不需要的部分,然后使用“回溯”模块中的函数根据需要格式化/打印。

于 2009-12-03T21:52:36.730 回答
2

您可能还对PEP-3134感兴趣,它在 python 3 中实现,允许您将一个异常/回溯附加到上游异常。

这与修改回溯并不完全相同,但它可能是将“短版本”传达给库用户同时仍然具有“长版本”可用的理想方式。

于 2012-09-12T05:24:01.203 回答
1

对于python3,这是我的答案。请阅读评论以获得解释:

def pop_exception_traceback(exception,n=1):
    #Takes an exception, mutates it, then returns it
    #Often when writing my repl, tracebacks will contain an annoying level of function calls (including the 'exec' that ran the code)
    #This function pops 'n' levels off of the stack trace generated by exception
    #For example, if print_stack_trace(exception) originally printed:
    #   Traceback (most recent call last):
    #   File "<string>", line 2, in <module>
    #   File "<string>", line 2, in f
    #   File "<string>", line 2, in g
    #   File "<string>", line 2, in h
    #   File "<string>", line 2, in j
    #   File "<string>", line 2, in k
    #Then print_stack_trace(pop_exception_traceback(exception),3) would print: 
    #   File "<string>", line 2, in <module>
    #   File "<string>", line 2, in j
    #   File "<string>", line 2, in k
    #(It popped the first 3 levels, aka f g and h off the traceback)
    for _ in range(n):
        exception.__traceback__=exception.__traceback__.tb_next
    return exception
于 2019-10-10T04:05:47.730 回答
0

您可能会对此代码感兴趣。

它需要回溯并删除不应显示的第一个文件。然后它模拟 Python 行为:

Traceback (most recent call last):

仅当回溯包含多个文件时才会显示。这看起来就像我的额外框架不存在一样。

这是我的代码,假设有一个字符串text

try:
    exec(text)
except:
    # we want to format the exception as if no frame was on top.
    exp, val, tb = sys.exc_info()
    listing = traceback.format_exception(exp, val, tb)
    # remove the entry for the first frame
    del listing[1]
    files = [line for line in listing if line.startswith("  File")]
    if len(files) == 1:
        # only one file, remove the header.
        del listing[0]
    print("".join(listing), file=sys.stderr)
    sys.exit(1)
于 2016-09-28T12:27:44.793 回答