为什么使用
Exception
创建的抽象 ABCMeta.register
的虚拟子类与 except
子句下不匹配?
我想确保我正在使用的包抛出的异常被转换为
MyException
,以便导入我的模块的代码可以使用 except MyException:
而不是 except Exception
捕获我的模块抛出的任何异常这样他们就不必依赖于实现细节(事实上我正在使用第三方包)。
为此,我尝试使用抽象基类将
OtherException
注册为 MyException
:
# Tested with python-3.6
from abc import ABC
class MyException(Exception, ABC):
pass
class OtherException(Exception):
"""Other exception I can't change"""
pass
MyException.register(OtherException)
assert issubclass(OtherException, MyException) # passes
try:
raise OtherException("Some OtherException")
except MyException:
print("Caught MyException")
except Exception as e:
print("Caught Exception: {}".format(e))
断言通过(如预期),但异常落在第二个块:
Caught Exception: Some OtherException
好吧,我又研究了一些。答案是,这是 Python3 中长期悬而未决的开放问题(从第一个版本开始就存在),并且显然是在 2011 年首次报告的。正如 Guido 在评论中所说的那样,“我同意这是一个错误,应该修复。”不幸的是,由于对修复性能和一些需要处理的极端情况的担忧,这个错误一直存在。 核心问题是
PyErr_GivenExceptionMatches
errors.c
使用
PyType_IsSubtype
而不是PyObject_IsSubclass
。由于类型和对象在 python3 中应该是相同的,这相当于一个错误。我已经对 python3 做了一个 PR
from abc import ABC
class MyException(Exception, ABC):
pass
class OtherException(Exception):
"""Other exception I can't change"""
pass
MyException.register(OtherException)
assert issubclass(OtherException, MyException) # passes
assert OtherException in MyException.__subclasses__() # fails
编辑:这个
assert
模仿了 except 子句的结果,但并不代表实际发生的情况。请参阅
接受答案以获取解释。解决方法也很简单:
class OtherException(Exception):
pass
class AnotherException(Exception):
pass
MyException = (OtherException, AnotherException)
__instancecheck__
except
方法。我们可以通过使用
和
__subclasscheck__
方法实现自定义元类来测试这一点:
class OtherException(Exception):
pass
class Meta(type):
def __instancecheck__(self, value):
print('instancecheck called')
return True
def __subclasscheck__(self, value):
print('subclasscheck called')
return True
class MyException(Exception, metaclass=Meta):
pass
try:
raise OtherException("Some OtherException")
except MyException:
print("Caught MyException")
except Exception as e:
print("Caught Exception: {}".format(e))
# output:
# Caught Exception: Some OtherException
我们可以看到元类中的
print
语句没有被执行。
我不知道这是否是有意/已记录的行为。我能找到的最接近相关信息的是来自
真正的这是否意味着类必须是
子类(即父类必须是子类的 MRO 的一部分)?我不知道。
至于解决方法:您可以简单地将MyException
设为
OtherException
的别名。class OtherException(Exception):
pass
MyException = OtherException
try:
raise OtherException("Some OtherException")
except MyException:
print("Caught MyException")
except Exception as e:
print("Caught Exception: {}".format(e))
# output:
# Caught MyException
如果您必须捕获多个没有公共基类的不同异常,您可以将
MyException
定义为元组:
MyException = (OtherException, AnotherException)
In [78]: class WithException:
...:
...: def __enter__(self):
...: pass
...: def __exit__(self, exc, msg, traceback):
...: if exc is OtherException:
...: raise MyException(msg)
...:
In [79]: with WithException():
...: raise OtherException('aaaaaaarrrrrrggggh')
...:
---------------------------------------------------------------------------
OtherException Traceback (most recent call last)
<ipython-input-79-a0a23168647e> in <module>()
1 with WithException():
----> 2 raise OtherException('aaaaaaarrrrrrggggh')
OtherException: aaaaaaarrrrrrggggh
During handling of the above exception, another exception occurred:
MyException Traceback (most recent call last)
<ipython-input-79-a0a23168647e> in <module>()
1 with WithException():
----> 2 raise OtherException('aaaaaaarrrrrrggggh')
<ipython-input-78-dba8b409a6fd> in __exit__(self, exc, msg, traceback)
5 def __exit__(self, exc, msg, traceback):
6 if exc is OtherException:
----> 7 raise MyException(msg)
8
MyException: aaaaaaarrrrrrggggh
只需从
Exception
创建基本例外。
import pytest
class MyExceptionBase(Exception):
...
class MyOtherException(MyExceptionBase):
...
class MyExceptionBase2(Exception):
...
class MyOtherException2(MyExceptionBase2):
...
@pytest.mark.parametrize(
"exception",
[
(MyExceptionBase),
(MyOtherException),
(MyExceptionBase2),
(MyOtherException2),
]
)
def test_my_exception(exception):
def my_raiser():
raise exception
with pytest.raises(exception):
try:
my_raiser()
except MyExceptionBase as e:
print("cause the base 1:", e.__class__.__name__)
raise e
except MyExceptionBase2 as e:
print("cause the base 2:", e.__class__.__name__)
raise e
except Exception as e:
print(e.__class__.__name__)
raise e
结果:
pytest -svv tests/unit/test_exceptions.py
tests/unit/test_exceptions.py::test_my_exception[MyExceptionBase] cause the base 1: MyExceptionBase
PASSED
tests/unit/test_exceptions.py::test_my_exception[MyOtherException] cause the base 1: MyOtherException
PASSED
tests/unit/test_exceptions.py::test_my_exception[MyExceptionBase2] cause the base 2: MyExceptionBase2
PASSED
tests/unit/test_exceptions.py::test_my_exception[MyOtherException2] cause the base 2: MyOtherException2
PASSED