Alex 在他写的“水禽和抽象基类”一文中指出,即便不注册,抽象基类也 能把一个类识别为虚拟子类。下面是他举的例子,我添加了一些代码, 使用 issubclass 做测试:
>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
经 issubclass 函数确认(isinstance 函数也会得出相同的结 论),Struggle 是 abc.Sized 的子类,这是因为 abc.Sized 实现了 一个特殊的类方法,名为 __subclasshook__。__subclasshook__会改变isinstance和issubclass的行为
class Sized(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
# 对 C.__mro__ (即 C 及其超类)中所列的类来说,如果类的__dict__ 属性中有名为 __len__ 的属性……
if any("__len__" in B.__dict__ for B in C.__mro__):
# 返回 True,表明 C 是 Sized 的虚拟子类。
return True
return NotImplemented
__subclasshook__ 在白鹅类型中添加了一些鸭子类型的踪迹。我们可 以使用抽象基类定义正式接口,可以始终使用 isinstance 检查,也可 以完全使用不相关的类,只要实现特定的方法即可(或者做些事情让 __subclasshook__ 信服)。当然,只有提供 __subclasshook__ 方 法的抽象基类才能这么做。
在自己定义的抽象基类中要不要实现 __subclasshook__ 方法呢?可 能不需要。我在 Python 源码中只见到 Sized 这一个抽象基类实现了 __subclasshook__ 方法,而 Sized 只声明了一个特殊方法,因此只 用检查这么一个特殊方法。鉴于 __len__ 方法的“特殊性”,我们基本 可以确定它能做到该做的事。但是对其他特殊方法和基本的抽象基类来 说,很难这么肯定。例如,虽然映射实现了 __len__、__getitem__ 和 __iter__,但是不应该把它们视作 Sequence 的子类型,因为不能 使用整数偏移值获取元素,也不能保证元素的顺序。当 然,OrderedDict 除外,它保留了插入元素的顺序,但是不支持通过 偏移获取元素。
在你我自己编写的抽象基类中实现 __subclasshook__ 方法,可靠性 很低。我可不相信随便一个实现或继承了 load、pick、inspect 和 loaded 的类(如 Spam)的行为一定像 Tombola。程序员最好让 Spam 继承 Tombola,至少也要注册(Tombola.register(Spam)),从而确 保这一点。当然,自己实现的 __subclasshook__ 方法还可以检查方 法签名和其他特性,但我觉得不值得这么做。