抽象基类和异常

问题描述 投票:0回答:5

问题

为什么使用

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
python python-3.x abc
5个回答
6
投票

好吧,我又研究了一些。答案是,这是 Python3 中长期悬而未决的开放问题(从第一个版本开始就存在),并且显然是在 2011 年首次报告的。正如 Guido 在评论中所说的那样,“我同意这是一个错误,应该修复。”不幸的是,由于对修复性能和一些需要处理的极端情况的担忧,这个错误一直存在。 核心问题是

PyErr_GivenExceptionMatches

中的异常匹配例程errors.c
使用
PyType_IsSubtype
而不是
PyObject_IsSubclass
。由于类型和对象在 python3 中应该是相同的,这相当于一个错误。

我已经对 python3 做了一个

PR

,似乎涵盖了线程中讨论的所有问题,但考虑到历史,我对它很快就会被合并并不非常乐观。我们拭目以待。


5
投票

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)



1
投票
__instancecheck__

子句中列出的类调用元类的

except
方法。

我们可以通过使用

__instancecheck__

__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

语句没有被执行。


我不知道这是否是有意/已记录的行为。我能找到的最接近相关信息的是来自
异常处理教程

except 子句中的类与异常兼容,如果它是 同一类或其基类

这是否意味着类必须是
真正的

子类(即父类必须是子类的 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)



0
投票

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



0
投票

只需从

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

	
© www.soinside.com 2019 - 2024. All rights reserved.