鸭子类型非正式的特征动态协议是 Python 中接口的“常规”方式,接口明确的抽象基类(ABC)是新知识,提供了严格规定和类型检查。

11.1 Python 文化中的接口和协议

  • 接口的含义:对象公开方法的子集,让对象在系统中扮演特定的角色。如“文件类对象”、“可迭代对象”并不是指特定的类,而是指实现了特定的公开方法
  • Python 接口、协议、类对象的含义是相同的——与继承无关

11.2 Python 的序列协议

  • 序列协议中的接口(参考抽象基类 collections.abc.Sequence)见下图:
    • 序列类型必须实现的方法(抽象方法): __len__(来自 Sized)、__getitem__
    • Sequence基于 __getitem__ 方法重写了继承的抽象方法 __contains____iter__,并实现了 __reversed__indexcount 方法。
  • 类似地,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 模块

  • IterableContainerSized所有集合形式类型的抽象基类,应当至少实现其协议(迭代、in 运算、len() 函数);
  • SequenceMappingSet 为主要的不可变类型,且有各自的可变子类;
  • MappingView映射类型:映射的方法 .items().keys().values() 分别会返回三种映射类型子类的实例(见上图右下角),包含丰富的接口;
  • CallableHashable 的作用是为内置函数 isinstance 提供支持
  • IteratorIterable 的子类,表示迭代器。

抽象数字类型

  • 抽象数字类型的层级结构:NumberComplexRealRationalIntegral

11.7 定义并使用一个抽象基类

抽象基类与子类的定义

  • 自己定义的抽象基类继承自 abc.ABC(在 Python 3.4 之前需要使用 metaclass=abc.ABCMeta,在 Python 2 中要使用类属性 __metaclass__ = abc.ABCMeta);
  • 抽象方法使用 @abc.abstractmethod 装饰(堆叠装饰器时应放在最底层),定义体内只有文档字符串
  • 抽象基类可以提供具体方法,需要依赖接口中的其他方法实现;
  • 注:IndexErrorKeyError(查找时可能的异常)是 LookupError 的子类,可统一捕获;
  • 小技巧:random.SystemRandom 类适用于“加密的随机序列”,其产生的序列是不可再生的,因为其随机性来自系统而非软件环境。

虚拟子类

不继承的情况下,可把一个类注册为抽象基类的虚拟子类

  • 使用抽象基类的 register 方法(或装饰器) 注册虚拟子类。可以使用 issubclassininstance 等函数识别,但不会继承任何方法或属性,需要实现所需的全部方法(不会在实例化时检查);
  • 类属性和所有方法继承的原理:类的继承关系使用类属性 __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__方法的可靠性很低,因为无法保证同名接口行为相同。