Python中子类关系的传递性

Python 中 type 与 object 的关系 这篇文章中,我提到了 Combine Solid Arrows Rule,即:

如果 A 是 B 的子类,且 B 是 C 的子类,那么 A 也是 C 的子类

也就是说子类关系(继承关系)具有传递性:

class C:
    pass

class B(C):
    pass

class A(B):
    pass

issubclass(A, B)
>>> True

issubclass(B, C)
>>> True

issubclass(A, C)
>>> True

A.__mro__
>>> (__main__.A, __main__.B, __main__.C, object)

但我们来看看下面的代码:

from collections import Hashable
issubclass(list, object)
>>> True

issubclass(object, Hashable)
>>> True

issubclass(list, Hashable)
>>> False

很明显,list 是不可哈希的

上面的代码貌似说明了子类关系并不一定具有传递性

但仔细观察会发现一个很不对劲的地方:issubclass(object, Hashable) 的结果为 True

难道 object 是 Hashable 的子类?当然不是了

我们知道在 Python 中 object 已经是”最原始”的父类了,它不继承任何类,它是所有类的父类:

print(object.__base__)
>>> None

object.__mro__
>>> (object,)

难道子类关系不一定具有传递性吗?我们来看看 Hashable 这个类的源码:

class Hashable(metaclass=ABCMeta): 

    __slots__ = ()

    @abstractmethod
    def __hash__(self):
        return 0

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Hashable:
            return _check_methods(C, "__hash__")
        return NotImplemented

注意到它的元类并不是 type,而是 ABCMeta,且定义了一个魔法方法 __subclasshook__

__subclasshook__ 实际上被元类 ABCMeta 的 __subclasscheck__ 调用:

class ABCMeta(type):
    def __subclasscheck__(cls, subclass):
        """Override for issubclass(subclass, cls)."""
        # Check cache
        # 省略部分代码
        # Check the subclass hook
        ok = cls.__subclasshook__(subclass) # 调用 __subclasshook__
        # 省略部分代码

这个方法有点长,这里省去了部分代码

它的注释(Override for issubclass(subclass, cls))告诉我们

__subclasscheck__ 其实就是 issubclass() 的魔法方法

当我们调用 issubclass(subclass, cls) 时,如果 cls 定义了 __subclasscheck__ (一般是其元类定义的)

那么 Python 就会调用 __subclasscheck__

__subclasscheck__ 会去调用 cls 的 __subclasshook__(一般由cls自己定义)

并把参数 subclass 传给 cls 的 __subclasshook__

这样就可以由 cls 自己来决定 subclass 是否为其子类

而这里的 Hashable 在它的 __subclasshook__ 中只是检查 subclass 是否定义了 __hash__

def _check_methods(C, *methods): 
    # 这里 methods 为 (__hash__,),C 为 __subclasscheck__ 传进来的 subclass
    mro = C.__mro__
    for method in methods:
        for B in mro: # 检查 C 及其所有父类是否含有该方法
            if method in B.__dict__: 
                if B.__dict__[method] is None: # 子类可以通过 __hash__ = None 成为不可哈希的
                    return NotImplemented
                break
        else:
            return NotImplemented
    return True

Hashable 认为只要 subclass 定义了 __hash__ 且不为 None,那么它就认为 subclass 是其子类

我个人认为这种检测方式是不合理的,其主要还是因为 Python 中并没有 接口 这种对象

有关 __subclasscheck__ 及 Python 中抽象基类的的引入可见 PEP319

class C:
    @classmethod
    def __subclasshook__(cls, C):
        return True

issubclass(object, C)
>>> False

C 并没声明元类,默认就是 type,而 type 并没有定义 __subclasscheck__

因此 __subclasshook__ 永远不会被调用

class MyMetaClass(type): # 自定义元类
    def __subclasscheck__(cls, subclass): # 直接就返回 True,并没有去调用 __subclasshook__
        return True

class C(metaclass=MyMetaClass): # 声明元类为自定义的元类 MyMetaClass
    @classmethod
    def __subclasshook__(cls, C): # 它只能在其元类的 __subclasscheck__ 中被调用
        return False

issubclass(object, C)
>>> True

可以看到真正起决定性的函数是 __subclasscheck__,而不是 __subclasshook__

所以按照协议,我们应该在 __subclasscheck__ 中调用 cls 的 __subclasshook__,这样就会把决定权交给 cls

这就是为什么一般我们会去使用 Python 给我们写好的 ABCMeta 而不是自定义元类:

from abc import ABCMeta
class C(metaclass=ABCMeta): # 声明元类为 ABCMeta
    @classmethod
    def __subclasshook__(cls, C):
        return True

issubclass(object, C)
>>> True

class C(metaclass=ABCMeta): # 声明元类为 ABCMeta
    @classmethod
    def __subclasshook__(cls, subclass): # 这里的 cls 就是 C 自己,而 subclass 是待检测的类
        if cls in subclass.__mro__:
            return True
        else:
            return False

issubclass(object, C) # 会去调用 C 的 __subclasshook__,object 作为参数 subclass
>>> False

class B(C):
    pass

issubclass(B, C)
>>> True

因此,我们可以看到,原本 Python 中的子类关系就是传递的

但自从引入 __subclasscheck__ 之后,这种子类关系的决定权就对用户开放了

这使得传递性可以被用户的代码所打破,这种自由在某种程度上来讲是一种缺陷和妥协

参考资料:

  1. Python Subclass Relationships Aren’t Transitive

  2. PEP319

文章目录
|