Python子类检查和子类钩子

66

__subclasscheck____subclasshook__方法用于确定一个类是否被视为另一个类的子类。但是,它们的文档非常有限,即使是在高级Python书籍中也是如此。它们应该如何使用,它们的区别是什么(更高的优先级、它们所指的关系的方向等)?

2个回答

77

这两种方法都可以用于定制内置函数issubclass()的结果。

__subclasscheck__

class.__subclasscheck__(self, subclass)

如果子类应该被视为(class)的(直接或间接)子类,则返回True。如果定义了,则调用以实现issubclass(subclass, class)

请注意,这些方法在类的类型(元类)上查找。它们不能在实际类中定义为类方法。这与在实例上调用的特殊方法的查找方式一致,只是在这种情况下,实例本身就是一个类。

这个方法是负责定制issubclass检查的特殊方法。正如“Note”所述,它必须在元类上实现!

class YouWontFindSubclasses(type):
    def __subclasscheck__(cls, subclass):
        print(cls, subclass)
        return False

class MyCls(metaclass=YouWontFindSubclasses):
    pass

class MySubCls(MyCls):
    pass

即使您拥有真正的子类,此实现也将返回False:

>>> issubclass(MySubCls, MyCls)
<class '__main__.MyCls'> <class '__main__.MySubCls'>
False

实际上,__subclasscheck__的应用还有更多有趣的用途。例如:
class SpecialSubs(type):
    def __subclasscheck__(cls, subclass):
        required_attrs = getattr(cls, '_required_attrs', [])
        for attr in required_attrs:
            if any(attr in sub.__dict__ for sub in subclass.__mro__):
                continue
            return False
        return True

class MyCls(metaclass=SpecialSubs):
    _required_attrs = ['__len__', '__iter__']

通过这种实现方式,任何定义了__len____iter__的类在issubclass检查中都会返回True

>>> issubclass(int, MyCls)  # ints have no __len__ or __iter__
False
>>> issubclass(list, MyCls)  # but lists and dicts have
True
>>> issubclass(dict, MyCls)
True

在这些示例中,我没有调用超类的__subclasscheck__,从而禁用了正常的issubclass行为(由type.__subclasscheck__实现)。但是重要的是要知道,您也可以选择仅扩展正常行为而不是完全覆盖它:
class Meta(type):
    def __subclasscheck__(cls, subclass):
        """Just modify the behavior for classes that aren't genuine subclasses."""
        if super().__subclasscheck__(subclass):
            return True
        else:
            # Not a normal subclass, implement some customization here.

__subclasshook__

__subclasshook__(subclass)

(必须定义为类方法。)

检查子类是否被认为是此ABC的子类。这意味着您可以进一步自定义issubclass的行为,而无需在要考虑作为ABC子类的每个类上调用register()。(此类方法从ABC的__subclasscheck__()方法中调用。)

此方法应返回TrueFalseNotImplemented。如果它返回True,则将子类视为此ABC的子类。如果返回False,则不将子类视为此ABC的子类,即使它通常是一个子类。如果返回NotImplemented,则继续使用通常的机制进行子类检查。

重要的是,它是在类上定义为classmethod,并且由abc.ABC.__subclasscheck__调用。因此,只有在处理具有ABCMeta元类的类时才能使用它:

import abc

class MyClsABC(abc.ABC):
    @classmethod
    def __subclasshook__(cls, subclass):
        print('in subclasshook')
        return True

class MyClsNoABC(object):
    @classmethod
    def __subclasshook__(cls, subclass):
        print('in subclasshook')
        return True

这只会进入第一个的__subclasshook__中:

>>> issubclass(int, MyClsABC)
in subclasshook
True

>>> issubclass(int, MyClsNoABC)
False

请注意,后续的issubclass调用不再进入__subclasshook__,因为ABCMeta缓存了结果:

>>> issubclass(int, MyClsABC)
True

请注意,通常您需要检查第一个参数是否是类本身。这样可以避免子类“继承”__subclasshook__而不是使用正常的子类确定。

例如(来自CPython collections.abc模块):

from abc import ABCMeta, abstractmethod

def _check_methods(C, *methods):
    mro = C.__mro__
    for method in methods:
        for B in mro:
            if method in B.__dict__:
                if B.__dict__[method] is None:
                    return NotImplemented
                break
        else:
            return NotImplemented
    return True

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

因此,如果您检查某个东西是否是Hashable的子类,它将使用由if cls is Hashable保护的自定义__subclasshook__实现。但是,如果您有一个实际实现Hashable接口的类,则不希望它继承__subclasshook__机制,而是希望使用正常的子类机制。
例如:
class MyHashable(Hashable):
    def __hash__(self):
        return 10

>>> issubclass(int, MyHashable)
False

尽管 int 实现了 __hash__,并且 __subclasshook__ 检查了 __hash__ 的实现,但在这种情况下的结果是 False。它仍然进入 Hashable__subclasshook__,但立即返回 NotImplemented,这表明应该使用正常实现进行处理。如果没有 if cls is Hashable,那么 issubclass(int, MyHashable) 将返回 True

何时应该使用 __subclasscheck____subclasshook__

这真的取决于具体情况。可以在类上实现 __subclasshook__ 而不是元类,但需要使用 ABCMeta(或 ABCMeta 的子类)作为元类,因为 __subclasshook__ 方法实际上只是 Python 的 abc 模块引入的约定。

您始终可以使用 __subclasscheck__,但必须在元类上实现它。

实际上,如果要实现接口(因为这些通常使用 abc),并且想要自定义子类机制,则使用 __subclasshook__。如果想要发明自己的约定(例如 abc 所做的),则使用 __subclasscheck__。因此,在正常情况下(不是有趣的情况下),99.99% 的情况下,您只需要 __subclasshook__


1
非常有用,感谢!但是你说:“如果没有 if cls is Hashable,则 issubclass(int,MyHashable) 将返回 True!”那有什么问题呢?我们正在执行结构子类型检查,即检查 int 的接口是否包括 MyHashable 的接口(尽管 _check_methods 仅检查方法的存在,忽略了它们的签名,这使得检查不完整,原因我不明白,但那是另一个话题),所以我们实际上期望 issubclass(int,MyHashable)True - Géry Ogam
实际上,如果默认情况下所有Python类都是协议,即如果Python默认切换到结构子类型(但是该想法已在PEP 544中被拒绝),我希望issubclass(int, MyHashable)True。但由于这不会发生,非协议类应使用命名子类型,因此我同意结构子类型应限制在具有if cls is Hashable保护的协议类中。再次感谢。 - Géry Ogam
@Maggyero 我可能错了,但是这个道理不是反过来也一样吗?从 issubclass(int, MyHashable) = true 可以得出 issubclass(int, Hashabel) = true(但不一定反过来成立)?但是,这确实有点难以理解(即使对我来说也是如此——请记住,我是4年前写的答案 :D),所以感谢你提出这个问题 :) - MSeifert
是的,你说得对,子类型关系是可传递的,因此 issubclass(int, MyHashable) is Trueissubclass(MyHashable, Hashable) is True 意味着 issubclass(int, Hashable) is True。不客气 :) - Géry Ogam
@Maggyero,是的,你的例子绝对正确。但这不是你之前说的。你之前的评论是issubclass(MyHashable, Hashable) is Trueissubclass(int, Hashable) is True,这意味着issubclass(int, MyHashable) is True - MSeifert
显示剩余3条评论

11
__subclasshook____subclasscheck__ 用于定制 issubclass 函数的行为。更多信息请参考 abc 源代码

__subclasscheck__ 会在类的类型(元类)上查找。普通类不应定义此方法。

__subclasshook__ 会检查子类是否被视为 ABC 的子类。这意味着可以进一步定制 issubclass 的行为,而无需在想要将其视为 ABC 子类的每个类上调用 register()。

也就是说,可以在 ABC 类中定义 __subclasshook__ 并设置某些条件,所有满足该条件的类都将被视为子类。

例如:

from abc import ABCMeta

class Sized(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Sized:
            if any("__len__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

class A(object):
    pass

class B(object):
    def __len__(self):
        return 0

issubclass(A, Sized)  # False
issubclass(B, Sized)  # True

4
为什么需要if cls is Sized:这一行代码? - vikkyhacks
1
@vikkyhacks 这样,当你子类化 Sized 并继承 __subclasshook__ 时,并不会将所有具有 __len__ 方法的类都视为该子类的子类。 - timgeb

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