面向对象编程全靠接口。在 Python 中,支撑一个类型的
是它提供的方法,也就是接口。
在不同的编程语言中,接口的定义和使用方式不尽相同。从 Python 3.8
开始,有 4 种方式,如图 13-1 中的类型图所示。这 4 种方式概述如下。
- 鸭子类型
自 Python 诞生以来默认使用的类型实现方式。从第 1 章开始,本书一直在研究鸭子类型。 - 大鹅类型
自 Python 2.6 开始,由抽象基类支持的方式,该方式会在运行时检查对象是否符合抽象基类的要求。大鹅类型是本章的主要话题。 - 静态类型
C
和Java
等传统静态类型语言采用的方式。自Python 3.5
开始,由
typing
模块支持,由符合“PEP 484—Type Hints”
要求的外部类型检查
工具实施检查。本章不涉及该方式。第 8 章的大多数内容和第 15 章讨论了静态类型。 - 静态鸭子类型
因Go
语言而流行的方式。由typing.Protocol
(Python 3.8
新增)的子类支持,由外部类型检查工具实施检查。静态鸭子类型首次出现在 8.5.10 节。
类型图
图 13-1 描述的 4 种类型实现方式各有优缺点,相辅相成,缺一不可。
图 13-1:上半部分是只使用 Python
解释器在运行时检查类型的方式;
下半部分则要借助外部静态类型检查工具,例如 MyPy
或 PyCharm
等
IDE
。左边两象限中的类型基于对象的结构(对象提供的方法),与
对象所属的类或超类无关;右边两象限中的类型要求对象有明确的类
型名称:对象所属类的名称,或者超类的名称。
这 4 种方式全都依靠接口,不过静态类型可以只使用具体类型实现(效
果差),而不使用协议和抽象基类等接口抽象。本章涵盖围绕接口实现
的 3 种类型:鸭子类型、大鹅类型和静态鸭子类型。
两种协议
在计算机科学中,根据上下文,“协议”一词有不同的含义。HTTP
这种
网络协议指明了客户端可向服务器发送的命令,例如 GET
、PUT
和
HEAD
。12.4 节讲过,对象协议指明为了履行某个角色,对象必须实现哪
些方法。第 1 章中的 FrenchDeck
示例演示了一个对象协议,即序列协
议:一个 Python
对象想表现得像一个序列需要提供的方法。
完全实现一个协议可能需要多个方法,不过,通常可以只实现部分协
议。下面以示例 13-1 中的 Vowels
类为例。
示例13-1 使用__getitem__
方法实现部分序列协议
只要实现 __getitem__
方法,就可以按索引获取项,以及支持迭代和
in
运算符。其实,特殊方法 __getitem__
是序列协议的核心。
如果对象提供序列协议,就返回 1
,否则返回 0
。注意,除了 dict
子类,如果一个 Python
类有 __getitem__()
方法,则也返回 1……
我们预期序列支持 len()
函数,也就是要实现 __len__
方法。Vowels
没有 __len__
方法,不过在某些上下文中依然算得上是序列。而有些
时候,这就足够了。所以,我经常说协议是“非正式接口”。第一个使
用“协议”这个术语的面向对象编程环境 Smalltalk
也是这么理解协议的。
- 动态协议
Python 一直有的非正式协议。动态协议是隐含的,按约定定义,在
文档中描述。Python 大多数重要的动态协议由解释器支持,在《Python
语言参考手册》的第 3 章“数据模型”中说明。 - 静态协议
“PEP 544—Protocols: Structural subtyping (static duck typing)”定义的
协议,自 Python 3.8 开始支持。静态协议要使用typing.Protocol
子 类显式定义。
二者之间的主要区别如下。
- 对象可以只实现动态协议的一部分,但是如果想满足静态协议,则对象必须提供协议类中声明的每一个方法,即使程序用不到。
- 静态协议可以使用静态类型检查工具确认,动态协议则不能。
两种协议共有一个基本特征:类无须通过名称(例如通过继承)声明支持什么协议。除了静态协议,Python 还提供了另一种定义显式接口的方式,即抽象基类。
利用鸭子类型编程
我们以 Python
中两个最重要的协议(序列协议和可迭代协议)为例展
开对动态协议的讨论。即使对象只实现了这些协议的最少一部分,也会
引起解释器的注意。
Python
喜欢序列
Python
数据模型的哲学是尽量支持基本的动态协议。对序列来说,即便
是最简单的实现,Python
也会力求做到最好。
图 13-2 展示的是通过一个抽象基类确立的 Sequence
接口。Python
解
释器和 list
、str
等内置序列根本不依赖那个抽象基类。我只是利用
它说明一个功能完善的序列应该支持什么操作。
图 13-2:Sequence
抽象基类和 collections.abc
中相关抽象类的
UML
类图。箭头由子类指向超类。以斜体显示的是抽象方法。Python3.6
之前的版本中没有 Collection
抽象基类,Sequence
是
Container
、Iterable
和 Sized
的直接子类
从图 13-2 可以看出,为了确保行为正确,Sequence
的子类必须实现
__getitem__
和 __len__
(来自 Sized
)。Sequence
中的其他方法都
是具体的,因此子类可以继承或者提供更好的实现。
再回顾一下示例 13-1 中的 Vowels
类。那个类没有继承
abc.Sequence
,而且只实现了 __getitem__
。
虽然没有 __iter__
方法,但是 Vowels
实例仍然可以迭代。这是因为
如果发现有 __getitem__
方法,那么 Python
就会调用它,传入从 0
开
始的整数索引,尝试迭代对象(这是一种后备机制)。尽管缺少
__contains__
方法,但是 Python
足够智能,能正确迭代 Vowels
实
例,因此也能使用 in 运算符:Python
做全面检查,判断指定的项是否
存在。
综上所述,鉴于序列类数据结构的重要性,如果没有 __iter__
方法和
__contains__
方法,则 Python
会调用__getitem__
方法,设法让迭
代和 in
运算符可用。
第 1 章定义的 FrenchDeck
类也没有继承 abc.Sequence
,但是实现了
序列协议的两个方法:__getitem__
和 __len__
。如示例 13-2 所示。
示例 13-2 一摞有序的纸牌(与示例 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
抽象基类的方法。例如,Sequence
类中的具体方法 __iter__
和 __contains__
是对 Python
解释器内置
行为的模仿。如果觉得好奇,可以到 Lib/_collections_abc.py
文件中阅读
这些方法的源码。
下面再分析一个示例,强调协议的动态本性,并解释静态类型检查工具
为什么没机会处理动态协议。
使用猴子补丁在运行时实现协议
猴子补丁在运行时动态修改模块、类或函数,以增加功能或修正 bug
。
例如,网络库 gevent
对部分 Python
标准库打了猴子补丁,不借助线程
或 async/await
实现一种轻量级并发。
示例 13-2 中的 FrenchDeck
类缺少一个重要功能:无法洗牌。几年
前,我在第一次编写 FrenchDeck
示例时实现了 shuffle
方法。后
来,在对 Python
风格有了深刻理解后我发现,既然 FrenchDeck
的行
为像序列,那么它就不需要 shuffle
方法,因为有现成的
random.shuffle
函数可用。根据文档,该函数的作用是“就地打乱序
列 x
”。
标准库中的 random.shuffle
函数用法如下所示。
标签:__,13,python,self,--,抽象,基类,def From: https://www.cnblogs.com/bonne-chance/p/18246500