繁体   English   中英

是否可以从已经捕获的外部代码中捕获异常?

[英]Is it possible to catch an exception from outside code that is already catching it?

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

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.')

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

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

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

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

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

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

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

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

这样,您将在exception_list中获得[('MyException', "Here's some specific info about my code hitting a problem"), ('Exception', 'The library code hit a problem')]

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

“稳健”的方法 - 通过__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__

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

这将返回有关当前异常的TracebackException object。 隐式链接的异常位于__context__中,显式链接的异常位于__cause__中。

请注意, __cause____context__都将返回TracebackException object 或None (如果您位于链的末尾)。 这意味着,您可以在返回值上再次调用__cause__ / __context__并基本上继续前进,直到到达链的末端。

打印TracebackException object 只是打印异常的消息,如果您想获取 class 本身(实际的 class,而不是字符串),您可以执行.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'>

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

注意:对于隐式链接的异常,如果一个异常被显式抑制,那么尝试恢复链将是糟糕的一天——无论如何,你可能会给__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进行回溯 object,您可能还记得上一个方法中从sys.exec_info返回的元组的第二个索引就是这样。 然后它返回帧object和 int ( Iterator[Tuple[FrameType, int]] ) 的元组生成器。

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

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

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

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

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

蔡斯上面的回答是惊人的。 为了完整起见,以下是我在这种情况下如何实施他们的第二种方法。 首先,我做了一个 function 可以在堆栈中搜索指定的错误类型。 尽管我的示例中的链接是隐式的,但这应该能够遵循隐式和/或显式链接:

import sys
import traceback

def find_exception_in_trace(exc_type):
    """Return latest exception of exc_type, or None if not present"""
    tb = traceback.TracebackException(*sys.exc_info())
    prev_exc = tb.__context__ or tb.__cause__
    
    while prev_exc:
        if prev_exc.exc_type == exc_type:
            return prev_exc
        prev_exc = prev_exc.__context__ or prev_exc.__cause__
    
    return None

有了它,它很简单:

while True:
    something = input('Enter something: ')
    try:
        library_function(my_callback, something)
    except LibraryException as exc:
        if (my_exc := find_exception_in_trace(MyException)):
            print(my_exc)
            continue
        raise exc

这样我就可以访问我的内部异常(并暂时打印它,尽管最终我可能会用它做其他事情)并继续。 但是,如果我的例外不在那里,我只需重新提出图书馆提出的任何问题。 完美的!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM