抽象基类和异常

8

问题

为什么使用ABCMeta.register创建的抽象Exception的虚拟子类在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
4个回答

6

好的,我进一步研究了一下。答案是这是Python3中存在已久的未解决问题(自首个版本以来就有这个问题),显然最初是在2011年报告的。正如Guido在评论中所说的那样,“我认为这是一个bug,应该修复。”不幸的是,由于对修复性能的担忧和需要处理的一些特殊情况,这个bug一直存在。

核心问题在于异常匹配程序PyErr_GivenExceptionMatches在{{link3:errors.c}}中使用PyType_IsSubtype而不是PyObject_IsSubclass。由于在Python3中类型和对象应该是相同的,因此这构成了一个bug。

我已经制作了一个针对Python3的PR(pull request), 它似乎涵盖了线程中讨论的所有问题,但考虑到历史原因,我并不是非常乐观它会很快合并。我们拭目以待。

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)

这里的“为什么”有点误导人... 解释器没有调用__subclasses__。相反,这是Python3中一个长期存在的错误。请参见被接受的答案。 - Mike McCoy

1

看起来CPython再次采取了一些捷径,并且不费力地调用元类中列在except子句中的类的__instancecheck__方法。

我们可以通过实现一个具有__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)

2
直到找到完美答案的解决方案之前,你还没有完全理解他的问题。 - MegaIng
这个解决方法对我的情况不起作用,因为它是从导入的第三方模块中的代码调用的。 - Mike McCoy
@MikeMcCoy 我不明白。我以为OtherException是在第三方模块中定义的,但MyException不是吗?你对MyException的定义有控制权,对吗? - Aran-Fey
@Aran-Fey 是的,但我认为他想捕获多个异常。看看我的编辑提交。 - MegaIng
@MegaIng 这个问题从来没有提到除了 OtherException 以外还有其他异常需要捕获,所以我对此表示怀疑。但无论如何,我已经添加了一个关于捕获多个不同异常的部分。 - Aran-Fey
@Aran-Fey 是的,它讨论了来自外部软件包的异常。 - MegaIng

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接