如何最好地过滤其__cause__(或__context__)上的异常?

how to best filter exceptions on their __cause__ (or __context__)?

提问人:calestyo 提问时间:7/14/2023 最后编辑:Karl Knechtelcalestyo 更新时间:7/15/2023 访问量:94

问:

给定基本异常类型:

class MyModuleError(Exception):
    pass

假设我们有代码使用异常链显式地引发它:

def foo():
    try:
        #some code
    except (ZeroDivisionError, OSError) as e:
        raise MyModuleError from e

现在,在调用代码中...

try:
    foo()
except MyModuleError as e:
    # Now what?

我怎样才能习惯地编写子句,以便异常处理依赖于(链式异常)?except__cause__

我想到了这些方法:

a) 使用类似:type(e)

    # filter here
    t=type(e.__cause__)
    if t is ZeroDivisionError:
        doStuff()
    elif t is OSError:
        doOtherStuff()
    else:
        raise

b) 使用类似:isinstance()

    # filter here
    if isinstance(e.__cause__, ZeroDivisionError):
        doStuff()
    elif isinstance(e.__cause__, OSError):
        doOtherStuff()
    else:
        raise

c) 再养,如:

    # filter here
    try:
        raise e.__cause__
    except ZeroDivisionError:
        doStuff()
    except OSError:
        doOtherStuff()
    except:
        raise e    #which should be the "outer" exception
python-3.x 异常

评论

2赞 wim 7/14/2023
选项b)对我来说似乎是最好的。a) 不考虑子类型,c) 在回溯中放置额外的垃圾
0赞 calestyo 7/14/2023
我刚刚注意到,这种方法似乎有记忆韭菜(我不明白为什么)......如果我用 调用它,python进程会占用整个系统内存(62GB),直到内核将其杀死^^wrapper3-n 100000000
0赞 Mad Physicist 7/14/2023
@wim。正要说同样的话
1赞 wim 7/14/2023
是的,对于两种类型,您不希望 isinstance。issubclass(PermissionError, OSError)
1赞 juanpa.arrivillaga 7/14/2023
基本上,(a) 和 (b) 是这里的两个合理的,具体取决于您想要用于运行时类型检查的语义(精确检查类型或使用哪个考虑子类型(以及覆盖的实例检查,例如 from ,尽管这更像是一种边缘情况)isinstance__subclasshook__

答:

0赞 calestyo 7/14/2023 #1

至少我问题的性能部分可以很容易地回答。

假设以下示例代码:

#!/usr/bin/python3

import timeit
import argparse


parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-n", "--iterations", type=int, default=1)
args = parser.parse_args()



class MyModuleError(Exception):
    pass

def foo():
    try:
        f = 1/0
    except (ValueError, ZeroDivisionError) as e:
        raise MyModuleError from e

def wrapper1():
    try:
        foo()
    except MyModuleError as e:
        t = type(e.__cause__)
        if t is ZeroDivisionError:
            pass
        elif t is OSError:
            pass
        else:
           raise

def wrapper2():
    try:
        foo()
    except MyModuleError as e:
        if isinstance(e.__cause__, ZeroDivisionError):
            pass
        elif isinstance(e.__cause__, OSError):
            pass
        else:
           raise

def wrapper3():
    try:
        foo()
    except MyModuleError as e:
        try:
            raise e.__cause__
        except ZeroDivisionError:
            pass
        except OSError:
            pass
        except:
           raise e    #which should be the "outer" exception



t = timeit.timeit(wrapper1, number=args.iterations)
print(f"wrapper1: {t}")
t = timeit.timeit(wrapper2, number=args.iterations)
print(f"wrapper2: {t}")
t = timeit.timeit(wrapper3, number=args.iterations)
print(f"wrapper3: {t}")

从 CPython 3.11.4 开始,给出以下结果:

$ ./chained-exception-handling.py -n 10000000
wrapper1: 4.373930287983967
wrapper2: 4.534742605988868
wrapper3: 7.319078870001249

其实,我有点失望......我认为重新引发内部异常的方法可能是最“pythonic”的方法,但这也是最慢的(会,有点适合 Python,不是吗?!O;-) ).

评论

0赞 calestyo 7/14/2023
请参阅我自己对原始问题的评论。 (出于某种原因我不明白)有内存泄漏,如果足够高,则被内核杀死进程。wrapper3-n
0赞 juanpa.arrivillaga 7/14/2023
重新引发异常以故意捕获它当然不是 pythonic。
1赞 juanpa.arrivillaga 7/14/2023
因此,当您捕获异常时,错误会携带对堆栈帧的引用,但堆栈帧会引用该错误。这会导致参考周期,从而导致内存泄漏。通常,作为特例,引用实际上会被删除,但像这样重新引发它可以使它保持活动状态。CPython 使用引用计数作为其主要的内存管理策略,但有一个处理引用周期的辅助 gc,但我很漂亮,它禁用了 !所以这可以解释内存泄漏etimeit.timeitgc
1赞 juanpa.arrivillaga 7/14/2023
这不是 CPython 错误,不是。我怀疑这只是一个问题,但它也不是错误,因为它禁用辅助垃圾收集器的事实被记录在案。要测试的一件事是在 timeit 之外的循环中运行,并查看足够高的 n 是否重现内存不足错误。timeit.timeitwrapper3()
1赞 juanpa.arrivillaga 7/14/2023
或者,按照文档的建议,添加为字符串'gc.enable()'setup
2赞 Karl Knechtel 7/15/2023 #2

不建议重新饲养。一般来说,故意举起一些东西,以便立即抓住它不是惯用的1.它的性能也较低(当实际引发异常时,异常处理可能会涉及相当多的开销),并在异常对象和当前堆栈帧之间创建一个引用循环,在 Python 的参考实现中,当辅助垃圾回收器被禁用时,这将泄漏内存。(这就是为什么在 3.x 中,在 except 块之后显式删除使用 as 创建的异常名称的原因。

在一般情况下,类型检查的首选方法,而不是直接比较结果,因为会自动考虑子类型。用于子分型的帐户的正常功能(例如 会抓住一个,这通常是可取的);按理说,在任何正常情况下,链式异常的类型检查也应该这样做。isinstancetypeisinstanceexceptexcept IOError:FileNotFoundError

没有明确的内置功能;因此,这里建议使用方法 b)。

1是的,for 循环是使用 StopIteration 在内部以这种方式实现的。这是一个实现细节,用户代码看起来不像是在做这样的事情。