简体   繁体   中英

Why does raising an exception invoke __subclasscheck__?

Consider the following example that uses __subclasscheck__ for a custom exception type:

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

class MyError(Exception, metaclass=MyMeta):
    pass

Now when raising an exception of this type, the __subclasscheck__ method gets invoked; ie raise MyError() results in:

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

Here the first line of the output shows that __subclasscheck__ got invoked to check whether MyError is a subclass of itself, ie issubclass(MyError, MyError) . I'd like to understand why that's necessary and how it's useful in general.


I'm using CPython 3.8.1 to reproduce this behavior. I also tried PyPy3 (3.6.9) and here __subclasscheck__ is not invoked.

I guess this is a CPython implementation detail. As stated in documentation to PyErr_NormalizeException :

Under certain circumstances, the values returned by PyErr_Fetch() below can be “unnormalized”, meaning that *exc is a class object but *val is not an instance of the same class.

So sometime during the processing of the raised error, CPython will normalize the exception, because otherwise it cannot assume that the value of the error is of the right type.

In your case it happens as follows:

  • Eventually while processing the exception, PyErr_Print is called, where it calls _PyErr_NormalizeException .
  • _PyErr_NormaliizeException calls PyObject_IsSubclass .
  • PyObject_IsSubclass uses __subclasscheck__ if it is provided.

I cannot say what those "certain circumstances" for " *exc is a class object but *val is not an instance of the same class" are (maybe needed for backward compatibility - I don't know).


My first assumption was, that it happens, when CPython ensures (ie here ), that the exception is derived from BaseException .

The following code

class OldStyle():
    pass

raise OldStyle

would raise OldStyle for Python2, but TypeError: exceptions must be old-style classes or derived from BaseException, not type for

class NewStyle(object):
    pass

raise NewStyle

or TypeError: exceptions must derive from BaseException in Python3 because in Python3 all classes are "new style".

However, for this check not PyObject_IsSubclass but PyType_FastSubclass is used:

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

ie only the tpflag s are looked at.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM