抽象基础 类 和异常

Abstract base classes and Exceptions

问题

为什么使用 ABCMeta.register 创建的摘要 Exception 的虚拟子 class 与 except 子句不匹配?

背景

我想确保将我正在使用的包抛出的异常转换为 MyException,以便导入我的模块的代码可以使用 [= 捕获我的模块抛出的任何异常17=] 而不是 except Exception 这样他们就不必依赖于实现细节(事实上我正在使用第三方包)。

例子

为此,我尝试使用抽象基础 class:

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

原因很简单:

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)

好吧,这并没有真正直接回答您的问题,但是如果您试图确保一段代码调用您的异常,您可以采取不同的策略,通过使用上下文管理器进行拦截。

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

CPython 似乎又一次走捷径,不再为 except 中列出的 classes 调用 metaclass 的 __instancecheck__ 方法条款。

我们可以通过使用 __instancecheck____subclasscheck__ 方法实现自定义元 class 来测试它:

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

我们可以看到 metaclass 中的 print 语句没有被执行。


我不知道这是否是 intended/documented 行为。我能找到的最接近相关信息的是 exception handling tutorial:

A class in an except clause is compatible with an exception if it is the same class or a base class thereof

这是否意味着 classes 必须是 real subclasses(即父 class 必须是subclass 的 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

如果您必须捕获多个没有共同基础的不同异常 class,您可以将 MyException 定义为一个元组:

MyException = (OtherException, AnotherException)

好的,我对此进行了更多调查。答案是它是 Python3 中的一个长期悬而未决的问题(自从第一次发布以来)并且显然是评论中的第一个 reported in 2011. As Guido said,"I agree it's a bug and should be fixed." 不幸的是,这个错误一直存在关注修复的性能和一些需要处理的极端情况。

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

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