Chapter 11 Interfaces - From Protocols to ABCs
鸭子类型非正式的特征动态协议是 Python 中接口的“常规”方式,接口明确的抽象基类(ABC)是新知识,提供了严格规定和类型检查。
11.1 Python 文化中的接口和协议
- 接口的含义:对象公开方法的子集,让对象在系统中扮演特定的角色。如“文件类对象”、“可迭代对象”并不是指特定的类,而是指实现了特定的公开方法;
- Python 接口、协议、类对象的含义是相同的——与继承无关。
11.2 Python 的序列协议
- 序列协议中的接口(参考抽象基类
collections.abc.Sequence)见下图:- 序列类型必须实现的方法(抽象方法):
__len__(来自Sized)、__getitem__; - 在
Sequence中基于__getitem__方法重写了继承的抽象方法__contains__和__iter__,并实现了__reversed__、index、count方法。
- 序列类型必须实现的方法(抽象方法):
- 类似地,Python 在对一般对象进行迭代、
in操作时,如果没有实现这些方法,Python 会调用__getitem__来让迭代、in可用(后备机制)。 只需要实现__getitem__方法即可实现迭代,这也是鸭子类型的极端形式:会尝试用其他的方法实现操作,能工作即为实现了该功能。

- 生成器是一次性的,无法使用
__len__、__getitem__等方法。而range()等函数生成的独有对象符合序列协议,可以使用序列的接口:
>>> range(5)[1]
1
>>> len(range(5))
5
>>> (x for x in range(5))[1]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable
11.3 使用猴子补丁在运行时实现协议
属性在运行时的动态替换,叫做猴子补丁(Monkey Patch)。
- 在设计方法时,首先考虑该实例的行为是否符合特定的协议。如果针对这一协议有现成的方法,则可以直接调用标准库(如对近似序列的类型进行打乱)。这是鸭子类型提供的优势:函数和方法的定义和实现是基于协议的,而不是基于实际类型的(无类型规定);
- 实例的属性、方法都是可以动态修改的;
- 每个 Python 方法都是普通参数,实例方法的参数命名为
self只是一种约定,没有实际意义。从这一角度来看,实例的方法和普通函数的语法是完全相同的,只是在变量作用域(如私有变量)等方面有不同。(见下面的代码)
class A:
def __init__(d):
d.s = 5
def getS(obj):
return obj.s
a = A()
A.S = getS
print(a.S()) # 5
print(getS(a)) # 5
11.4 白鹅类型
鸭子类型:忽略对象的真正类型,转而关注对象有没有实现所需的方法、签名和语义; 白鹅类型:避免不相关的类型有同名而含义不同的方法,借鉴水禽的“支序系统学”,引入抽象基类。
isinstance通过抽象基类来判断是否是实例类型,解决了上述问题。
class Struggle:
def __len__(self):
return 23
from collections import abc
print(isinstance(Struggle(), abc.Sized))
- 不应当滥用
isinstance检查来区分类型:在使用抽象基类时,可以借助多态来对不同类别的实例对象实现正确的方法调用; - 在基于协议进行分类操作时,可以直接进行处理而不进行类型判断,这样可以充分利用鸭子类型的特点来简化操作,将调用成功与失败作为标准来间接分类(当规模很大或是一次性数据类型时,使用抽象基类的类型判断更好)。
11.5 定义抽象基类的子类
- Python 会在实例化继承自抽象基类的子类对象时,检查是否实现了所有的抽象方法,若没有实现则会抛出
TypeError异常; - 可以利用子类的特点,覆盖从抽象基类中继承的非抽象方法,以更高效的方式重新实现。
11.6 标准库中的抽象基类
collections.abc 模块

Iterable、Container、Sized为所有集合形式类型的抽象基类,应当至少实现其协议(迭代、in运算、len()函数);Sequence、Mapping、Set为主要的不可变类型,且有各自的可变子类;MappingView是映射类型:映射的方法.items()、.keys()、.values()分别会返回三种映射类型子类的实例(见上图右下角),包含丰富的接口;Callable、Hashable的作用是为内置函数isinstance提供支持;Iterator为Iterable的子类,表示迭代器。
抽象数字类型
- 抽象数字类型的层级结构:
Number、Complex、Real、Rational、Integral。
11.7 定义并使用一个抽象基类
抽象基类与子类的定义
- 自己定义的抽象基类继承自
abc.ABC(在 Python 3.4 之前需要使用metaclass=abc.ABCMeta,在 Python 2 中要使用类属性__metaclass__ = abc.ABCMeta); - 抽象方法使用
@abc.abstractmethod装饰(堆叠装饰器时应放在最底层),定义体内只有文档字符串; - 抽象基类可以提供具体方法,需要依赖接口中的其他方法实现;
- 注:
IndexError和KeyError(查找时可能的异常)是LookupError的子类,可统一捕获; - 小技巧:
random.SystemRandom类适用于“加密的随机序列”,其产生的序列是不可再生的,因为其随机性来自系统而非软件环境。
虚拟子类
在不继承的情况下,可把一个类注册为抽象基类的虚拟子类。
- 可使用抽象基类的
register方法(或装饰器) 注册虚拟子类。可以使用issubclass和ininstance等函数识别,但不会继承任何方法或属性,需要实现所需的全部方法(不会在实例化时检查); - 类属性和所有方法继承的原理:类的继承关系使用类属性
__mro__(Method Resolution Order)指定,它列出了该类和所有超类,不会包含虚拟继承的抽象基类。注意:实例属性不会被继承,这也是super().__init__()的功能(调用父类的初始化方法); - 小技巧:声明实例方法时,直接使用语句
m_name = func即可(方法和函数完全相同,除了它是类属性,与类属性的定义方式相同)。
11.8 子类的测试方法
- 类方法
__subclasses__()返回类的直接子类列表(是在内存中存在的直接子代,因此取决于模块中导入的类),不包含虚拟子类; - 抽象基类独有的类属性
_abc_registry是注册的虚拟子类弱引用的集合(WeakSet)。
11.9 Python 使用 register 的方式
- 更常见的注册方法是使用函数注册其他地方定义的类。写在全局作用域中,这样在导入模块时,解释器会执行这些注册的语句。
11.10 鹅的行为有可能像鸭子
- 有的抽象基类可以自行识别类是否为虚拟子类,这依赖基类特殊的类方法
__subclasshook__(cls, C),定义了如何判断子类C是否实现了接口(是虚拟子类); - 这种自行检查虚拟子类的方式与鸭子类型相似(只判断接口,而不考虑类继承);
- 自己实现
__subclasshook__方法的可靠性很低,因为无法保证同名接口行为相同。