python - 是否可以从已经捕获的外部代码中捕获异常?
问题描述
这是一个很难表达的问题,但这里有一个精简版的情况。我正在使用一些接受回调的库代码。它有自己的错误处理,如果在执行回调时出现任何问题,就会引发错误。
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 的方法是什么?
解决方案
似乎有很多方法可以实现您想要的。不过,哪个更健壮-我找不到线索。我将尝试解释所有对我来说似乎很明显的方法。也许您会发现其中之一很有用。
我将使用您提供的示例代码来演示这些方法,这里有一个更新鲜的外观 -
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__
这首先构造TracebackException
from sys.exc_info
。sys.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
提供每一帧的字典。在第一个tb
fromtb_gen
中,您将看到MyException
作为当地人的一部分....在许多其他东西中。
我有一种令人毛骨悚然的感觉,我忽略了一些方法,很可能是使用inspect
. 但我希望上述方法足够好,以至于没有人必须经历混乱inspect
:P
推荐阅读
- javascript - 如何在reactjs中一次只检查一个复选框?
- python - 在python中将字符串转换为谷歌搜索链接
- sql - 如何在条件下在连接中使用特定表?
- excel - 根据文本框值在列表框中显示过滤结果
- nlog - 使用 NLog,我可以使用另一个 LayoutRender 的值来设置 LayoutRender 的属性吗?
- javascript - 如何改进仅垂直砌体网格算法?
- javascript - 在 ReactNative 中,按一下空格会产生很大的空间
- android - 在 linux Ubuntu 中为 android 构建 Brave 浏览器
- python - 如何在批次输入上设置条件以仅训练批次中的特定样本而不是 pytorch 中的完整批次?
- ios - 如果我可以为苹果智能手表开发应用程序来获取原始 PPG 数据并实现我自己的心率检测算法