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

huangapple go评论75阅读模式
英文:

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

问题

try:
    # filter here
    t = type(e.__cause__)
    if t is ZeroDivisionError:
        doStuff()
    elif t is OSError:
        doOtherStuff()
    else:
        raise
try:
    # filter here
    if isinstance(e.__cause__, ZeroDivisionError):
        doStuff()
    elif isinstance(e.__cause__, OSError):
        doOtherStuff()
    else:
        raise
try:
    # filter here
    try:
        raise e.__cause__
    except ZeroDivisionError:
        doStuff()
    except OSError:
        doOtherStuff()
    except:
        raise e    #which should be the "outer" exception
英文:

Given a base exception type:

class MyModuleError(Exception):
    pass

Suppose we have code that explicitly raises it, using exception chaining:

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

Now, in the calling code...

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

how can I idiomatically write the except clause, so that the exception handling depends on the __cause__ (chained exception)?

I thought of these approaches:

a) using type(e) like:

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

b) using isinstance() like:

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

c) re-raising like:

    # filter here
    try:
        raise e.__cause__
    except ZeroDivisionError:
        doStuff()
    except OSError:
        doOtherStuff()
    except:
        raise e    #which should be the "outer" exception

答案1

得分: 2

尝试:
    foo()
除非 MyModuleError as e:
    # 这里筛选
    如果 isinstance(e.__cause__, ZeroDivisionError):
        做一些事情()
    elif isinstance(e.__cause__, OSError):
        做其他事情()
    else:
        提高

一般来说,通常认为使用 isinstance() 比使用 type() 进行类型检查更加灵活且推荐。

英文:
try:
    foo()
except MyModuleError as e:
    # filter here
    if isinstance(e.__cause__, ZeroDivisionError):
        doStuff()
    elif isinstance(e.__cause__, OSError):
        doOtherStuff()
    else:
        raise

in general, using isinstance() is often considered more flexible and recommended over using type() for type checking.

答案2

得分: 2

抛出异常不被推荐。通常,故意抛出异常以便立即捕获它不是惯用做法。这也不够高效(异常处理在实际抛出异常时可能涉及相当多的开销),并且在异常对象和当前堆栈帧之间创建了一个引用循环,在Python的参考实现中,当禁用辅助垃圾收集器时,会导致内存泄漏。 (这就是为什么使用as创建的异常名称在3.x中在except块之后被明确删除的原因。)

根据一般原则,isinstance 是首选的方法 来进行类型检查,而不是直接比较type的结果,因为isinstance会自动考虑子类型。except的正常功能会考虑子类型(例如,except IOError:会捕获FileNotFoundError,这通常是可取的);因此,在任何正常情况下,链式异常的类型检查也应该这样做。

没有针对此的显式内置功能;因此,在这里推荐使用方法b)。

英文:

The re-raising is not recommended. In general, it is not idiomatic to raise something deliberately so that it can be immediately caught<sup>1</sup>. It's also less performant (exception handling can involve quite a bit of overhead when the exception is actually raised), and creates a reference cycle between the exception object and the current stack frame, which in the reference implementation of Python will leak memory when the auxiliary garbage collector is disabled. (This is why exception names created with as are explicitly deleted after the except block in 3.x.)

On general principle, isinstance is the preferred means of type-checking rather than comparing type results directly, because isinstance automatically takes subtyping into account. The normal functionality of except accounts for subtyping (e.g. except IOError: will catch a FileNotFoundError, which is normally desirable); it stands to reason that, in any normal circumstance, type-checking of the chained exception should do this as well.

There is no explicit built-in functionality for this; therefore, approach b) is recommended here.

<sub><sup>1</sup>Yes, for loops are internally implemented this way, using StopIteration. That's an implementation detail, and user code isn't intended to look like it's doing such things.</sub>

答案3

得分: 0

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

假设以下示例代码:

#!/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;-))。

英文:

At least the performance part of my question can be answered quite easily.

Assuming the following example code:

#!/usr/bin/python3

import timeit
import argparse


parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument(&quot;-n&quot;, &quot;--iterations&quot;, 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 &quot;outer&quot; exception



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

Gives, as of CPython 3.11.4 the following results:

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

Actually, I'm a bit disappointed... I thought the method of re-raising the inner exception might be the most "pythonic" one, but that's also the slowest (will, kinda fits Python, doesn't it?! O;-) ).

huangapple
  • 本文由 发表于 2023年7月14日 09:34:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76684195.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定