英文:
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("-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}")
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;-) ).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论