[英]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_locals
( Tuple[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.