第十三章 接口、协议和 抽象基类

面向接口编程,不要面向实现编程

                      -----  Gamma, Helm, Johnson, Vlissides, First Principle of Object-Oriented Design

面向对象的编程都是关于接口的。理解 Python 中的类型的最佳方法是了解它提供的方法——也就是这个类的接口——如 “Types are defined by supported operations”(第 8 章)中所述。

根据编程语言,我们至少有一种定义和使用接口的方法。在python中,从 Python 3.8 开始,我们有四种方式定义和使用接口。它们在类型图(图 13-1)中进行了描述。我们可以这样总结它们:

  • 鸭子类型:Python一直在用的类型的默认方法。从第 1 章开始,我们就一直在研究鸭子类型。
  • 天鹅类型:自 Python 2.6 起,由抽象基类 (ABC) 支持的方法,这些方法依赖于基于ABC 实现的对象的运行时检查。天鹅类型 是本章的一个主要主题。
  • 静态类型:C 和 Java 等静态类型语言的传统方法;自 Python 3.5 起由typing模块支持,并由符合  PEP 484—Type Hints的外部类型检查器强制执行。第 8 章和后面的第 15 章的大部分内容都是关于静态类型的。
  • 静态鸭子类型:一种 Go 语言流行的方法;由 Typing.Protocol 的子类支持 - Python 3.8 中的新功能 - 也由外部类型检查器强制执行。我们最初在“静态协议”(第 8 章)中看到了静态鸭子类型。

类型映射

这四种类型方法是补充:它们各有优缺点。不能取消支持他们中的任何一个。

这四种方法中的每一种都依赖于接口来工作,但静态类型可以只使用具体的类型,而不是抽象的接口,如协议和抽象基类。本章是关于鸭子类型、天鹅类型和静态鸭子类型——围绕接口的类型规则。

本章分为四个顶级部分,针对类型映射中四个象限中的三个(图 13-1):

  • “Two kinds of protocols”将两种结构类型协议进行了比较——即类型映射的左侧部分(鸭子类型和静态鸭子类型)。
  • “Programming ducks”深入探讨了 Python 常用的鸭子类型,包括如何使其更安全,同时保持其主要优势:灵活性。
  • “Goose typing” 解释了使用 ABC 进行更严格的运行时类型检查。这是最长的一节,不是因为它更重要,而是因为在本书的其他地方有更多关于鸭子类型、静态鸭子类型和静态类型的部分。
  • “Static protocols”涵盖了typing.Protocol的使用、实现和设计——对于静态和运行时类型检查很有用。

本章的新内容

本章经过大量编辑,比第一版中相应的第 11 章大概增加了24%的内容。虽然有些小节和很多段落是一样的,但有很多新的内容。这些是亮点:

  • 章节介绍和类型映射(图 13-1)是全新的内容。这是本章中大多数新内容的关键——以及与 Python ≥ 3.8 中typing相关的所有其他章节。
  • “两种协议”解释了动态协议和静态协议的异同。
  • “防御性编程和“快速失败”主要复制了第一版的内容,但进行了更新,现在有一个独立的章节来强调其重要性。
  • “静态协议”是全新的。它建立在“静态协议”(第 8 章)中的演示基础上。
  • 更新了图 13-2、图 13-3 和图 13-4 中 collections.abc 的 UML 类图,以包含 Python 3.6 中添加的 Collection ABC。

第一版中有一个部分鼓励使用numbers ABC 的天鹅类型。在“The numbers ABCs and numeric protocols”,中,我解释了如果你打算使用静态类型检查器同时使用天鹅类型风格的运行时检查,为什么你应该使用 Typing 模块提供的数字静态协议。

Python中两种“不同”协议

根据上下文的不同,协议这个词在计算机科学中有不同的含义。网络协议(例如 HTTP)指定客户端可以发送到服务器的命令,例如 GET、PUT 和 HEAD。我们在“协议和鸭子类型”中看到,对象协议指定了对象必须提供的方法来履行角色。第 1 章中的 FrenchDeck 示例演示了一个对象协议,即序列协议:允许 Python 对象表现为序列的方法。

实现一个完整的协议可能需要几种方法,但通常只实现其中的一部分就可以了。例如这个Vowels类:

例 13-1。使用 __getitem__ 实现部分序列协议。

>>> class Vowels:
...     def __getitem__(self, i):
...         return 'AEIOU'[i]
...
>>> v = Vowels()
>>> v[0]
'A'
>>> v[-1]
'U'
>>> for c in v: print(c)
...
A
E
I
O
U
>>> 'E' in v
True
>>> 'Z' in v
False

实现 __getitem__ 足以允许通过索引检索项,并且还支持迭代和 in 运算符。__getitem__ 特殊方法确实是序列协议的关键。从 Python/C API 参考手册的Sequence Protocol部分查看: 

int PySequence_Check(PyObject *o)

Return 1 if the object provides sequence protocol, and 0 otherwise. Note that it returns 1 for Python classes with a __getitem__() method unless they are dict subclasses […]

如果对象提供序列协议,则返回 1,否则返回 0。请注意,除非它们是 dict 子类,否则它为提供 __getitem__() 方法的 Python 类型返回 1 

我们希望序列也能通过实现 __len__ 来支持 len()。Vowels没有 __len__ 方法,但它在某些上下文中仍然表现为序列。这可能足以满足我们的目的。这就是为什么我喜欢说协议是一个“非正式接口”。这也是 Smalltalk 中对协议的理解方式,Smalltalk是第一个使用该术语的面向对象的编程环境。

除了有关网络编程的文档,Python 文档中“协议”一词的大多数用法都是指这些非正式接口。

现在,随着 PEP 544—Protocols: Structural subtyping (static duck typing) 的采用,“协议”这个词在 Python 中又出现另一个含义——两个含义密切相关,但又有所不同的含义。PEP 544 允许我们继承types.Protocol 来定义一个或多个类必须实现(或继承)包含静态类型检查器的方法。

当我需要区分时,我将采用以下术语:

动态协议:Python 一直拥有的非正式协议。动态协议是隐式的,由约定进行定义并在文档加以描述。Python 最重要的动态协议由解释器本身支持,并记录在 Python 语言参考的 “Data Model” chapter 中。

静态协议: 自 Python 3.8 起,PEP 544—Protocols: Structural subtyping (static duck typing) 定义的协议。静态协议有一个明确的定义:一个 Typing.Protocol 子类。

它们之间有两个主要区别:

  1. 如果一个对象只实现了动态协议的一部分,那么他仍然有可能正常使用;但是要实现静态协议,对象必须提供在协议类中声明的每个方法,即使您的程序不需要它们。
  2. 静态协议可以通过静态类型检查器进行验证,但动态协议不能。

两种协议都有一个基本特征,即类从不需要通过在命名时(即通过继承)声明它支持协议。

除了静态协议之外,Python 还提供了另一种在代码中定义显式接口的方法:抽象基类 (ABC)。

本章的其余部分涵盖动态和静态协议,以及 ABC。

鸭子编程 

让我们从 Python 中最重要的两个协议开始讨论动态协议:序列和可迭代协议。解释器不遗余力地处理那些提供这些协议的最小实现的对象,如下一节所述。

Python 会尽力支持序列

Python 数据模型的理念是尽量支持最基本的协议。对序列来说,即使是最简单的实现,Python 也会力求做到最好。

图 13-2 显示了形式化为 ABC的Sequence 接口。Python 解释器和一些内置的序列类型(如 list、str 等)也不依赖于这些ABC。我仅使用它来描述一个完整的 Sequence 应该支持的内容。

TIP: 

collections.abc 模块中的大多数 ABC 用于形式化由内置对象实现和由解释器隐式支持的接口 - 两者都早于 ABC 本身。ABC 可用作新类的起点,并支持运行时的显式类型检查(也称为天鹅类型)以及静态类型检查器的类型提示。

研究图 13-2,我们看到 Sequence 的正确子类必须实现 __getitem__ 和 __len__(来自 Sized)。Sequence 中的所有其他方法都是具体方法,因此子类可以继承以使用这些方法——或者提供更好的实现。

现在,回忆一下示例 13-1 中的Vowels类。它没有集成 abc.Sequence抽象类并且只实现了 __getitem__方法。

即使没有实现 __iter__ 方法,但Vowels实例是可迭代的,因为作为备选方案​​,如果 Python 找到 __getitem__ 方法,它会从索引0开始,尝试通过调用该方法来迭代对象。尽管没有实现__contains__ 方法,但是由于Python 足够智能,能够迭代Vowels实例,因此可以使用 in 操作符:python会进行顺序扫描以检查项是否存在。

总之,考虑序列协议的重要性,如果没有 __iter__ 和 __contains__ 方法时,Python 设法通过调用 __getitem__ 来使迭代和 in 运算符可用。

例 13-2。作为Card序列的Deck(与示例 1-1 相同)

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]

第 1 章中的几个示例之所以有效,是因为 Python 对任何类似序列的对象给予了特殊处理。Python 中的可迭代协议代表了鸭子类型的一种极端形式:解释器尝试使用两种不同的方法来迭代对象。

需要明确的是:我在本节中描述的行为是在解释器本身中实现的,主要是使用 C 语言实现的。它们不依赖于来自 Sequence ABC 的方法。例如,Sequence 类中的具体方法 __iter__ 和 __contains__ 模拟了 Python 解释器的内置行为。如果您好奇,请查看 Lib/_collections_abc.py中这些方法的源代码。

现在让我们研究另一个强调协议动态特性的例子——以及为什么静态类型检查器没有机会处理它们。

使用猴子补丁在运行时实现协议

Note:

Monkey 补丁是在运行时动态更改模块、类或函数,以添加功能或修复错误.因为它不像常规补丁那样更改源代码,所以猴子补丁只影响当前运行程序的实例。gevent 网络库使用猴子补丁修改了Python 标准库的部分内容,以允许没有线程或async/await 的轻量级并发。请注意,猴子补丁取决于修补代码的实现细节,因此当库更新时它们很容易被破坏。

示例13-2 中的 FrenchDeck 类缺少一个基本特征:它不能被洗牌。多年前,当我第一次编写 FrenchDeck 示例时,我确实实现了一个 shuffle 方法。后来我有了 Pythonic 的见解:如果 FrenchDeck 表现得像一个序列,那么它不需要自己的 shuffle 方法,因为已经有 random.shuffle,如documented中的“将序列 x 就地打乱”。

标准库的 random.shuffle 函数是这样使用的:

>>> from random import shuffle
>>> l = list(range(10))
>>> shuffle(l)
>>> l
[5, 2, 9, 7, 8, 3, 1, 4, 0, 6]

TIP:

当您遵循既定的协议时,由于运行时的鸭子类型协议,您可以改进代码以利用现有标准库和第三方代码,避免重复造车。

然而,如果我们尝试打乱一个 FrenchDeck 实例,我们会得到一个异常,如例 13-3 所示。

例 13-3。 random.shuffle 无法处理 FrenchDeck

>>> from random import shuffle
>>> from frenchdeck import FrenchDeck
>>> deck = FrenchDeck()
>>> shuffle(deck)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../random.py", line 265, in shuffle
    x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment

错误消息很明确:“'FrenchDeck' object does not support item assignment('FrenchDeck' 对象不支持为元素赋值。)”问题是 shuffle 需要通过交换集合内的项目就地操作,而 FrenchDeck 只实现了不可变序列协议。可变序列还必须提供 __setitem__ 方法。

因为 Python 是动态语言,我们可以在运行时修复这个问题,甚至在交互式控制台中也是如此。示例 13-4 显示了如何执行此操作。

例 13-4。 使用猴子补丁修复 FrenchDeck 以使其可变并与 random.shuffle 兼容(从示例 13-3 继续)

>>> def set_card(deck, position, card):  1
...     deck._cards[position] = card
...
>>> FrenchDeck.__setitem__ = set_card  2
>>> shuffle(deck)  3
>>> deck[:5]
[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4',
suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]
  1. 创建一个函数,将deck、position和card作为参数。
  2. 将该函数分配给 FrenchDeck 类中名为 __setitem__ 的属性。
  3. 现在可以洗牌了,因为我添加了可变序列协议的必要方法。

特殊方法__setitem__ 的签名在 “3.3.6. Emulating container types”中定义.在这里,我将参数命名为deck、position、card——而不是语言参考中的 self、key、value——以表明每个 Python 方法都是作为一个普通函数开始的,命名第一个参数 self 只是一个约定。这在控制台会话中是可以的,但在 Python 源文件中,最好使用文档中的 self、key 和 value。

关键是 set_card 知道deck 对象有一个名为_cards 的属性,而_cards 必须是一个可变序列。然后将 set_card 函数作为 __setitem__ 特殊方法附加到 FrenchDeck 类。这是猴子补丁的一个例子:在运行时改变一个类或模块,而不触及源代码。猴子补丁功能强大,但进行实际修补的代码与要修补的程序非常紧密地结合在一起,通常会处理私有和未文档化的属性。

除了作为猴子补丁的一个例子之外,示例 13-4 还强调了鸭子类型中协议是动态的这一特性:random.shuffle 不关心参数的类型,它只需要对象实现了可变序列协议中的方法。对象是一开始就具备必要的方法还是后来以某种方式获得都无关紧要。

鸭子类型不会非常不安全也不会难以调试。下一节将展示一些有用的代码模式,无需借助显式检查即可检测动态协议。

防御式编程和“快速失败”

防御性编程就像防御性驾驶:一套即使粗心的程序员或司机也能提高安全性的实践。许多异常只能在运行时才能被捕获——即使在主流静态类型语言中也是如此。在动态类型语言中,“快速失败”是使程序更安全、更易于维护的极好建议。快速失败意味着尽快抛出运行时异常,例如,在函数体的最开始的位置就检验参数的合法性。

这是一个示例:当您编写接收一个序列并在内部按照一个列表进行处理的方法时,不要对序列参数进行类型检查。相反,获取参数并立即从中构建一个列表。这种代码模式的一个例子是示例 13-10 中的 __init__ 方法,示例出现在本章后面:

def __init__(self, iterable):
        self._balls = list(iterable)

这样就可以让你的代码更灵活,因为 list() 构造函数能够接受任意可迭代对象。如

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值