【问题标题】:Why does raising an exception invoke __subclasscheck__?为什么引发异常会调用 __subclasscheck__?
【发布时间】:2020-09-16 21:46:09
【问题描述】:

考虑以下将__subclasscheck__ 用于自定义异常类型的示例:

class MyMeta(type):
    def __subclasscheck__(self, subclass):
        print(f'__subclasscheck__({self!r}, {subclass!r})')

class MyError(Exception, metaclass=MyMeta):
    pass

现在,当引发这种类型的异常时,会调用 __subclasscheck__ 方法;即raise MyError() 结果:

__subclasscheck__(<class '__main__.MyError'>, <class '__main__.MyError'>)
Traceback (most recent call last):
  File "test.py", line 8, in <module>
    raise MyError()
__main__.MyError

这里输出的第一行显示__subclasscheck__ 被调用以检查MyError 是否是其自身的子类,即issubclass(MyError, MyError)。我想了解为什么这是必要的,以及它总体上是如何有用的。


我正在使用 CPython 3.8.1 来重现此行为。我还尝试了 PyPy3 (3.6.9),这里 __subclasscheck__ 没有被调用。

【问题讨论】:

    标签: python python-3.x exception cpython


    【解决方案1】:

    我猜这是一个 CPython 实现细节。如PyErr_NormalizeException的文档中所述:

    在某些情况下,PyErr_Fetch() 返回的值 下面可以“非规范化”,这意味着 *exc 是一个类对象,但 *val 不是同一类的实例。

    所以在处理引发的错误的某个时候,CPython 会对异常进行规范化,否则它不能假定错误的值是正确的类型。

    在你的情况下,它发生如下:

    • 最终在处理异常时,PyErr_Print 被调用,它在哪里 calls _PyErr_NormalizeException
    • _PyErr_NormaliizeExceptioncallsPyObject_IsSubclass.
    • PyObject_IsSubclass 使用 __subclasscheck__(如果提供)。

    我不能说“*exc 是类对象但*val 不是同一类的实例”的那些“某些情况”是什么(可能需要向后兼容 - 我不知道)。


    我的第一个假设是,当 CPython 确保(即here)异常源自BaseException 时,它会发生。

    以下代码

    class OldStyle():
        pass
    
    raise OldStyle
    

    会为 Python2 引发 OldStyle,但为 TypeError: exceptions must be old-style classes or derived from BaseException, not type 引发

    class NewStyle(object):
        pass
    
    raise NewStyle
    

    TypeError: exceptions must derive from BaseException 在 Python3 中,因为在 Python3 中所有类都是“新样式”。

    但是,此检查不是使用PyObject_IsSubclass,而是使用PyType_FastSubclass

    #define PyExceptionClass_Check(x)                                       \
        (PyType_Check((x)) &&                                               \
         PyType_FastSubclass((PyTypeObject*)(x), Py_TPFLAGS_BASE_EXC_SUBCLASS))
    

    即仅查看 tpflags。

    【讨论】:

    • 感谢您的彻底调查。实际上,执行子类检查的似乎是_PyErr_NormalizeException,但是当我使用except MyError 时也会触发它,即不打印异常。我想更好地了解PyErr_Fetch() 在什么情况下可以返回“非规范化”异常。我想这种情况不能从纯 Python 代码中创建?但是,确保一切设置正确难道不是 C 扩展的责任吗?另外我不确定这是否可以被视为实现细节,因为它执行任意用户代码。
    • @a_guest 显然,PyErr_Print 不是唯一调用 _PyErr_NormalizeException 的地方,它也会发生,例如在exception_unwindgithub.com/python/cpython/blob/…)中,当有except MyError时使用。
    • @a_guest “但是确保一切设置正确不是 C 扩展的责任”。不是,这就是我正在谈论的实现细节。
    • 所以我想这回答了我的问题的第一部分(“为什么这是必要的”;因为PyErr_Fetch 可能返回一个非实例,非无值,尽管这没有明确记录)。但是我也想了解“这通常如何有用”,即为什么 Python C-API 允许扩展提供非规范化异常?那有什么用例?此外,由于 CPython 和 PyPy 的 行为 不同,其中一个肯定是错误的,并且鉴于此子类检查不是 Python 规范的一部分,它似乎是 CPython。你同意吗?
    • @a_guest PyType_FastSubclass 只能用于某些预定义类(预定义类列表:github.com/python/cpython/blob/…),不能用于任何任意类。为了使 tpflag-check 工作,它必须在 C 代码中设置,就像这里 (github.com/python/cpython/blob/…)
    猜你喜欢
    • 2016-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-29
    相关资源
    最近更新 更多