获取完整的回溯信息

34

在以下情况下,如何获取完整的回溯信息,包括func2func函数的调用?

import traceback

def func():
    try:
        raise Exception('Dummy')
    except:
        traceback.print_exc()

def func2():
    func()


func2()

运行时出现以下结果:

Traceback (most recent call last):
  File "test.py", line 5, in func
    raise Exception('Dummy')
Exception: Dummy
< p >traceback.format_stack() 不是我想要的,因为我需要将 traceback 对象传递给第三方模块。

我特别关注这种情况:

import logging


def func():
    try:
        raise Exception('Dummy')
    except:
        logging.exception("Something awful happened!")


def func2():
    func()


func2()

在这种情况下,我得到:

ERROR:root:Something awful happened!
Traceback (most recent call last):
  File "test.py", line 9, in func
    raise Exception('Dummy')
Exception: Dummy

@Nathan,请仔细重新阅读问题。需要完整的回溯信息。 - warvariuc
2
请查看Graham Dupleton的博客文章在Python中为异常生成完整的堆栈跟踪 - Piotr Dobrogost
请解释一下你所说的“traceback.format_stack()不是我想要的,因为需要将traceback对象传递给第三方模块。” 请标明哪些代码来自第三方,哪些代码来自您自己。请使用适当的方法名称。 - guettli
2
也许相关:https://bugs.python.org/issue9427,“logging.error('...', exc_info=True)应该显示更多的上层帧”。 - guettli
@guettli logger.log使用exc_info = sys.exc_info(),其中包含有问题的回溯信息,并将此信息传递给Sentry处理程序,后者解析它以检查每个帧中的本地变量。是的,我认为问题是相同的。 - warvariuc
3
Python 3中增加此功能的进展正在Add traceback.print_full_exception()问题追踪中跟踪。 - Piotr Dobrogost
5个回答

40
正如mechmind所回答的那样,堆栈跟踪仅包括在引发异常的位置和try块的位置之间的帧。如果需要完整的堆栈跟踪,似乎您没什么办法。
除非从顶层到当前帧提取堆栈条目是显然可行的——traceback.extract_stack可以很好地处理它。问题是通过直接检查堆栈帧来获取的traceback.extract_stack的信息在任何时候都没有创建traceback对象,并且logging API需要一个traceback对象才能影响traceback输出。
幸运的是,logging不需要一个实际的traceback对象,它需要一个可以传递给traceback模块的格式化程序的对象。traceback也不关心——它只使用traceback的两个属性,帧和行号。因此,应该可以创建一个鸭子类型的伪traceback对象链表并将其作为traceback传递。
import sys

class FauxTb(object):
    def __init__(self, tb_frame, tb_lineno, tb_next):
        self.tb_frame = tb_frame
        self.tb_lineno = tb_lineno
        self.tb_next = tb_next

def current_stack(skip=0):
    try: 1/0
    except ZeroDivisionError:
        f = sys.exc_info()[2].tb_frame
    for i in xrange(skip + 2):
        f = f.f_back
    lst = []
    while f is not None:
        lst.append((f, f.f_lineno))
        f = f.f_back
    return lst

def extend_traceback(tb, stack):
    """Extend traceback with stack info."""
    head = tb
    for tb_frame, tb_lineno in stack:
        head = FauxTb(tb_frame, tb_lineno, head)
    return head

def full_exc_info():
    """Like sys.exc_info, but includes the full traceback."""
    t, v, tb = sys.exc_info()
    full_tb = extend_traceback(tb, current_stack(1))
    return t, v, full_tb

有了这些功能,你的代码只需要进行微小的修改:
import logging

def func():
    try:
        raise Exception('Dummy')
    except:
        logging.error("Something awful happened!", exc_info=full_exc_info())

def func2():
    func()

func2()

...以得到预期的输出:

ERROR:root:Something awful happened!
Traceback (most recent call last):
  File "a.py", line 52, in <module>
    func2()
  File "a.py", line 49, in func2
    func()
  File "a.py", line 43, in func
    raise Exception('Dummy')
Exception: Dummy

请注意,虚假回溯对象完全可用于内省 - 显示本地变量或作为 pdb.post_mortem() 的参数 - 因为它们包含对真实堆栈帧的引用。

2
我知道这个信息。如果你再看一下我的问题,你会发现我使用的是logging.exception,它是你所写的快捷方式。 - warvariuc
1
@warwaruk 你说得很对。你的问题点在于try/except块会缩短堆栈跟踪,而你想要完整的跟踪信息。我简直不敢相信我以前从未注意到这一点。 - user4815162342
3
我已经更新了答案以真正回答你的问题。如果有更简单的方法,我也想知道。 - user4815162342
哇,你做了很多工作。我也在考虑同样的方向,但我不敢相信没有更简单的方法... 我会接受你的答案,但如果有人发布更好的答案 - 我可能会接受其他答案。谢谢! - warvariuc
1
@warwaruk 我不明白为什么它不能工作 - 模拟的回溯包含对真实堆栈帧的引用。您可以通过使用full_exc_info()生成的回溯来调用pdb.post_mortem()轻松测试它。 - user4815162342
显示剩余2条评论

6

这是基于用户4815162342的答案,但更加简约:

import sys
import collections

FauxTb = collections.namedtuple("FauxTb", ["tb_frame", "tb_lineno", "tb_next"])

def full_exc_info():
    """Like sys.exc_info, but includes the full traceback."""
    t, v, tb = sys.exc_info()
    f = sys._getframe(2)
    while f is not None:
        tb = FauxTb(f, f.f_lineno, tb)
        f = f.f_back
    return t, v, tb

这种方式避免了抛出虚拟异常,但需要使用 sys._getframe()。它假设在捕获异常的 except 子句中使用,在两个堆栈帧(full_exc_info 和调用 full_exc_info 函数的函数,也就是调用引发代码的函数,并且已包含在原始回溯中)上升。

这将给出与 user4815162342 的答案相同的输出结果。

如果您不介意格式上的轻微差异,也可以使用此方法。

import logging

def func():
    try:
        raise Exception('Dummy')
    except:
        logging.exception("Something awful happened!", stack_info=True)

def func2():
    func()

func2()

这导致了

ERROR:root:Something awful happened!
Traceback (most recent call last):
  File "test.py", line 5, in func
    raise Exception('Dummy')
Exception: Dummy
Stack (most recent call last):
  File "test.py", line 12, in <module>
    func2()
  File "test.py", line 10, in func2
    func()
  File "test.py", line 7, in func
    logging.exception("Something awful happened!", stack_info=True)

在这种情况下,你将会得到一个从尝试到异常的跟踪迹线,并且第二个是从根调用到日志调用位置的跟踪迹线。

1
在我看来,你的第二个建议是最好的答案。它让你知道什么是异常跟踪和什么只是堆栈跟踪。唯一的问题是我不知道它是否会“作为回溯对象发送给第三方”。 - bballdave025
1
你的第一个答案也很好,因为我不喜欢抛出虚假异常的想法,即使在纯理论的情况下。 - bballdave025
不错的回答。在Python 3中,最好使用dataclass而不是命名元组(命名元组被过度使用)。还要注意,在Python 2中不存在stack_info - user4815162342
+1 bballdave025: 我可能有一个有点XY问题的情况,但在我的情况下,解决方案似乎只是mylogger.exception("Something awful happened!", stack_info=True)。(文档。) - undefined

3

当异常错误冒泡时,会收集堆栈跟踪信息。因此,您应该在所需的堆栈顶部打印回溯:

import traceback

def func():
    raise Exception('Dummy')

def func2():
    func()


try:
    func2()
except:
    traceback.print_exc()

4
问题在于这会改变语义。如果提问者希望在 func 失败时让 func2 继续运行(并记录回溯信息),异常必须在 func2 中处理,而不是在外部处理。 - user4815162342

2
我已经编写了一个模块,可以写出更完整的回溯信息。
该模块在这里:链接,文档在这里:链接
(你也可以从pypi获取该模块)
sudo pip install pd

要捕获和打印异常,请执行以下操作:

import pd

try:
    <python code>
except BaseException:       
    pd.print_exception_ex( follow_objects = 1 )

堆栈跟踪看起来像这样:
Exception: got it

#1  def kuku2(self = {'a': 42, 'b': [1, 2, 3, 4]}, depth = 1) at      t  test_pd.py:29
Calls next frame at:
    raise Exception('got it') at: test_pd.py:29

#2  def kuku2(self = {'a': 42, 'b': [1, 2, 3, 4]}, depth = 2) at test_pd.py:28
Calls next frame at:
    self.kuku2( depth - 1 ) at: test_pd.py:28

#3  def kuku2(self = {'a': 42, 'b': [1, 2, 3, 4]}, depth = 3) at test_pd.py:28
Calls next frame at:
    self.kuku2( depth - 1 ) at: test_pd.py:28

#4  def kuku2(self = {'a': 42, 'b': [1, 2, 3, 4]}, depth = 4) at test_pd.py:28
Calls next frame at:
    self.kuku2( depth - 1 ) at: test_pd.py:28

#5  def kuku2(self = {'a': 42, 'b': [1, 2, 3, 4]}, depth = 5) at     test_pd.py:28
 Calls next frame at:
    self.kuku2( depth - 1 ) at: test_pd.py:28

#6  def kuku2(self = {'a': 42, 'b': [1, 2, 3, 4]}, depth = 6) at test_pd.py:28
Calls next frame at:
    self.kuku2( depth - 1 ) at: test_pd.py:28

#7  def main() at test_pd.py:44
Local variables:
n = {'a': 42, 'b': [1, 2, 3, 4]}
Calls next frame at:
    pd.print_exception_ex( follow_objects = 1 ) at: test_pd.py:44

当follow_objects = 0时,不会打印出对象的内容(对于具有复杂数据结构的对象,跟随对象可能需要很长时间)。


文件"F:\Python\lib\site-packages\pd\pdd.py",第18行 返回值 ^ TabError:缩进中制表符和空格的使用不一致 - user

-1

从traceback中可以提取更多信息,而我有时候更喜欢一个更整洁、更“逻辑”的信息,而不是由traceback给出的带有文件、行号和代码片段的多行blob。最好一行就说出所有要点。

为了实现这个目标,我使用以下函数:

def raising_code_info():
    code_info = ''
    try:    
        frames = inspect.trace()
        if(len(frames)):
            full_method_name = frames[0][4][0].rstrip('\n\r').strip()
            line_number      = frames[1][2]
            module_name      = frames[0][0].f_globals['__name__']
            if(module_name == '__main__'):
                module_name = os.path.basename(sys.argv[0]).replace('.py','')
            class_name = ''
            obj_name_dot_method = full_method_name.split('.', 1)
            if len(obj_name_dot_method) > 1:
                obj_name, full_method_name = obj_name_dot_method
                try:
                    class_name = frames[0][0].f_locals[obj_name].__class__.__name__
                except:
                    pass
            method_name = module_name + '.'
            if len(class_name) > 0:
                method_name += class_name + '.'
            method_name += full_method_name
            code_info = '%s, line %d' % (method_name, line_number)
    finally:
        del frames
        sys.exc_clear()
    return code_info

它提供了.和行号,例如:

(示例模块名称:test.py):

(line 73:)
def function1():
    print 1/0

class AClass(object):    
    def method2(self):
        a = []
        a[3] = 1

def try_it_out():
    # try it with a function
    try:
        function1()
    except Exception, what:
        print '%s: \"%s\"' % (raising_code_info(), what)

    # try it with a method
    try:
        my_obj_name = AClass()
        my_obj_name.method2()       
    except Exception, what:
        print '%s: \"%s\"' % (raising_code_info(), what)

if __name__ == '__main__':
     try_it_out()


test.function1(), line 75: "integer division or modulo by zero"
test.AClass.method2(), line 80: "list assignment index out of range"

在某些使用情况下可能会更整洁一些。


我还没有检查你的代码,但我相信它可以工作。但问题中指出:“traceback.format_stack()不是我想要的,因为需要将traceback对象传递给第三方模块。”无论如何,感谢你提供另一个代码片段。 - warvariuc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接