【问题标题】:How can I modify a Python traceback object when raising an exception?引发异常时如何修改 Python 回溯对象?
【发布时间】:2010-12-08 21:41:42
【问题描述】:

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

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

提前感谢您的任何建议!

【问题讨论】:

    标签: python traceback


    【解决方案1】:

    从 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()
    

    【讨论】:

      【解决方案2】:

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

      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
      

      【讨论】:

      • 你如何使用这个?在引发之前,异常的__tracebackNone,在引发之后,我认为引发代码无法修改它。
      【解决方案3】:

      您可能会对这段代码感兴趣。

      它需要回溯并删除不应显示的第一个文件。然后它模拟 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)
      

      【讨论】:

      • 这里的重点是“模拟”——这不会改变抛出异常的回溯(sys.exit)。
      • 是的,但它显示了如何修改回溯。对自己的一点思考不会受到伤害;-)
      • 相当多的人对此投入了相当多的思考,但我还没有找到可行的解决方案。所以请不要暗示你的回答+“一点思考”会很容易回答OP的问题。
      • 接受的答案是关于“不更改回溯”。我的回答也假设了,然后这只是对字符串列表的一些摆弄,这很容易而不是追溯修改。
      • 是的,接受的答案确实说明了这一点。但是问题标题和正文都明确表示相反,要“修改回溯”。
      【解决方案4】:

      看看jinja2在这里做了什么:

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

      它很丑,但它似乎可以做你需要做的事情。这个例子太长了,我就不在这里复制粘贴了。

      【讨论】:

      【解决方案5】:

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

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

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

      【讨论】:

      • 知道如何在 python 3 中管理这个吗?我自己无法让它工作。
      • 在 Python 3 中的等价物是:raise ei[0](ei[1]).with_traceback(ei[2].tb_next)
      【解决方案6】:

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

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

      【讨论】:

        【解决方案7】:

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

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

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

        【讨论】:

        • 我投了反对票,原因如下:这个答案提供了很好的建议,但实际上并没有回答问题。
        • @BryanOakley:如果建议非常相关,并且如果它太大而无法放入评论中,那么它属于答案。要么就是这样,要么让知识远离 SO,这将是非常可悲的。所以我认为在这些情况下投反对票是不合适的。
        • @max:我想你是对的。我发现自己也在回答这样的问题——“你可以这样做,但是为什么……还有其他事情要考虑”。谢谢你让我负责。如果可以的话,我会改变我的投票。
        • 必须不同意“[it] 没有意义,因为回溯中的最后一行本身不能引发异常”的论点。拨打a += 1。它看起来“不能”引发异常(不包含单词raise)。但是,如果astr 类型,那么它会发生这种情况 - 并且异常是有意义的。现在假设a 是一个带有__iadd__ 的自定义类的实例,并且就像str 一样,操作数是int 是没有意义的。使用相同的语法,回溯现在将指向更深的位置,而不是错误所在的行。
        • 拒绝投票,因为我需要出于正当理由修改堆栈跟踪才能找到它。问题:“你如何修改回溯”。答案:“你没有”。相反,我会建议一种方法来做到这一点,同时强烈强调你不应该这样做。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-09-11
        • 2021-12-26
        • 1970-01-01
        • 1970-01-01
        • 2012-07-10
        • 1970-01-01
        • 2012-02-07
        相关资源
        最近更新 更多