首页 > 解决方案 > 是否可以从已经捕获的外部代码中捕获异常?

问题描述

这是一个很难表达的问题,但这里有一个精简版的情况。我正在使用一些接受回调的库代码。它有自己的错误处理,如果在执行回调时出现任何问题,就会引发错误。

class LibraryException(Exception):
    pass

def library_function(callback, string):
    try:
        # (does other stuff here)
        callback(string)
    except:
        raise LibraryException('The library code hit a problem.')

我在输入循环中使用此代码。我知道回调函数中可能出现的潜在错误,具体取决于输入中的值。如果发生这种情况,我想在从其错误消息中获得有用的反馈后重新提示。我想它看起来像这样:

class MyException(Exception):
    pass

def my_callback(string):
    raise MyException("Here's some specific info about my code hitting a problem.")

while True:
    something = input('Enter something: ')
    try:
        library_function(my_callback, something)
    except MyException as e:
        print(e)
        continue

当然,这是行不通的,因为MyException会被 捕获library_function,这将引发它自己的(信息量少得多)Exception并停止程序。

显而易见的事情在调用之前验证我的输入library_function,但这是一个循环问题,因为解析是我首先使用库代码的目的。(对于好奇的人来说,它是Lark,但我认为我的问题对 Lark 来说不够具体,不足以保证将所有具体细节弄得一团糟。)

一种替代方法是更改​​我的代码以捕获任何错误(或至少是库生成的错误类型),并直接打印内部错误消息:

def my_callback(string):
    error_str = "Here's some specific info about my code hitting a problem."
    print(error_str)
    raise MyException(error_str)

while True:
    something = input('Enter something: ')
    try:
        library_function(my_callback, something)
    except LibraryException:
        continue

但我看到了两个问题。一个是我撒了一张大网,可能会捕捉和忽略我所瞄准的范围以外的错误。除此之外,打印错误消息,然后将异常本身扔到空虚中,这似乎是……不优雅且不习惯。加上命令行事件循环仅用于测试;最终我计划将它嵌入到 GUI 应用程序中,并且没有打印输出,我仍然希望访问和显示有关问题所在的信息。

实现这样的事情的最干净和最 Pythonic 的方法是什么?

标签: pythonerror-handling

解决方案


似乎有很多方法可以实现您想要的。不过,哪个更健壮-我找不到线索。我将尝试解释所有对我来说似乎很明显的方法。也许您会发现其中之一很有用。

我将使用您提供的示例代码来演示这些方法,这里有一个更新鲜的外观 -

class MyException(Exception):
    pass

def my_callback(string):
    raise MyException("Here's some specific info about my code hitting a problem.")

def library_function(callback, string):
    try:
        # (does other stuff here)
        callback(string)
    except:
        raise Exception('The library code hit a problem.')

最简单的方法 -traceback.format_exc

import traceback

try:
    library_function(my_callback, 'boo!')
except:
    # NOTE: Remember to keep the `chain` parameter of `format_exc` set to `True` (default)
    tb_info = traceback.format_exc()   

这不需要太多关于异常和堆栈跟踪本身的知识,也不需要您将任何特殊的帧/回溯/异常传递给库函数。但是看看这会返回什么(如 的值tb_info)-

'''
Traceback (most recent call last):
  File "path/to/test.py", line 14, in library_function
    callback(string)
  File "path/to/test.py", line 9, in my_callback      
    raise MyException("Here's some specific info about my code hitting a problem.")       
MyException: Here's some specific info about my code hitting a problem.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "path/to/test.py", line 19, in <module>
    library_function(my_callback, 'boo!')
  File "path/to/test.py", line 16, in library_function
    raise Exception('The library code hit a problem.')
Exception: The library code hit a problem.
'''

那是一个字符串,如果你只是让异常发生而不捕获,你会看到同样的事情。注意这里的异常链接,顶部的异常是在底部的异常之前发生的异常。您可以解析出所有异常名称-

import re
exception_list = re.findall(r'^(\w+): (\w+)', tb_info, flags=re.M)

有了它,你就可以进去[('MyException', "Here's some specific info about my code hitting a problem"), ('Exception', 'The library code hit a problem')]exception_list

虽然这是最简单的方法,但它不是很了解上下文。我的意思是,你得到的只是字符串形式的类名。无论如何,如果这符合您的需求 - 我并不认为这有什么问题。

“稳健”的方法 - 通过__context__/递归__cause__

Python 本身会跟踪异常跟踪历史、当前手头的异常、导致此异常的异常等等。您可以在PEP 3134中阅读有关此概念的复杂细节

无论您是否通读 PEP 的全部内容,我都敦促您至少熟悉隐式链接异常显式链接异常。也许这个 SO 线程对此有用。

作为一个小复习,raise ... from用于显式链接异常。您在示例中显示的方法是隐式链接

现在,您需要记住 -TracebackException#__cause__用于显式链接异常TracebackException#__context__用于隐式链接异常。由于您的示例使用隐式链接,您可以简单地__context__向后跟随,您将到达MyException. 事实上,由于这只是一层嵌套,您将立即达到它!

import sys
import traceback

try:
    library_function(my_callback, 'boo!')
except:
    previous_exc = traceback.TracebackException(*sys.exc_info()).__context__

这首先构造TracebackExceptionfrom sys.exc_infosys.exc_info返回(exc_type, exc_value, exc_traceback)手头异常的元组(如果有)。请注意,按照特定顺序,这 3 个值正是您需要构建的TracebackException- 因此您可以简单地使用解构它*并将其传递给类构造函数。

这将返回一个TracebackException关于当前异常的对象。隐式链接的异常是 in __context__显式链接的异常是 in __cause__

请注意,两者都__cause____context__返回一个TracebackException对象,或者None(如果您位于链的末尾)。这意味着,您可以在返回值上再次__cause__调用/ ,并且基本上一直持续到到达链的末尾。__context__

打印一个TracebackException对象只是打印异常的消息,如果你想获取类本身(实际的类,而不是字符串),你可以这样做.exc_type

print(previous_exc)
# prints "Here's some specific info about my code hitting a problem."
print(previous_exc.exc_type)
# prints <class '__main__.MyException'>

这是一个递归.__context__并打印隐式链中所有异常类型的示例。(你可以对 做同样的事情.__cause__

def classes_from_excs(exc: traceback.TracebackException):
    print(exc.exc_type)
    if not exc.__context__:
        # chain exhausted
        return
    classes_from_excs(exc.__context__)

让我们使用它!

try:
    library_function(my_callback, 'boo!')
except:
    classes_from_excs(traceback.TracebackException(*sys.exc_info()))

那将打印-

<class 'Exception'>
<class '__main__.MyException'>

再一次,这样做的重点是要了解上下文。理想情况下,打印不是您在实际环境中想要做的事情,您拥有类对象本身,以及所有信息!

注意:对于隐式链接异常,如果显式抑制异常,那么尝试恢复链将是糟糕的一天-无论如何,您可能会__supressed_context__试一试。

痛苦的路——走过traceback.walk_tb

这可能是最接近低级异常处理的东西了。如果您想捕获整个信息帧而不仅仅是异常类和消息等,您可能会发现它walk_tb很有用……但有点痛苦。

import traceback

try:
    library_function(my_callback, 'foo')
except:
    tb_gen = traceback.walk_tb(sys.exc_info()[2])

有....这里讨论的太多了。.walk_tb需要一个回溯对象,您可能还记得上一个方法中返回的元组的第二个索引sys.exec_info就是这样。然后它返回一个由 frame 对象和 int ( )组成的元组的生成器。Iterator[Tuple[FrameType, int]]

这些框架对象具有各种复杂的信息。不过,无论您是否真的能找到您正在寻找的东西,都是另一回事。它们可能很复杂,但除非您进行大量帧检查,否则它们并不详尽。无论如何,这就是框架对象所代表的

你用框架做什么取决于你。它们可以传递给许多函数。您可以将整个生成器传递StackSummary.extract给获取 framesummary 对象,您可以遍历每一帧以查看[0].f_locals[0]onTuple[FrameType, int]返回实际的帧对象)等等。

for tb in tb_gen:
    print(tb[0].f_locals)

这将为您locals提供每一帧的字典。在第一个tbfromtb_gen中,您将看到MyException作为当地人的一部分....在许多其他东西中。

我有一种令人毛骨悚然的感觉,我忽略了一些方法,很可能是使用inspect. 但我希望上述方法足够好,以至于没有人必须经历混乱inspect:P


推荐阅读