主要内容
- 首先,本章说明抽象基类的常见用途:实现接口时作为超类使用
- 说明抽象基类如何检查具体子类是否符合接口定义,以及如何使 用注册机制声明一个类实现了某个接口,而不进行子类化操作
- 最后, 说明如何让抽象基类自动“识别”任何符合接口的类——不进行子类化或注册。
补充知识点
第十章&第十一章:
- 协议,指来自父类的继承关系、或者接口的强制性约束要求
- 鸭子类型:看起来像只鸭子,吃起来是鸭肉,无论它是否是鸭蛋孵化出来的,那么它就是鸭子。在示例10-3中,FrenchDeck没有继承序列父类,但是还是自己实现了len和getitem方法,可以像序列类(继承 abc.Sequence)一样使用,就够了。
- 我们发现 Python 对序列协议的支持十分深入。如果一个 类实现了 getitem 方法,此外什么也没做,那么 Python 会设法迭 代它,而且 in 运算符也随之可以使用。
- “白鹅类型”,可以使用抽象基类明确声明接 口,而且类可以子类化抽象基类或使用抽象基类注册(无需在继承关系 中确立静态的强链接),宣称它实现了某个接口。
其他
- 转换命令: jupyter nbconvert --to markdown E:PycharmProjectsTianChiProject 0_山枫叶纷飞competitions 13_fluent_pythonCH.11_面向对象_接口:从协议到抽象基类.ipynb
11.1 Python文化中的接口和协议
- Python 语言没有 interface 关键字,,而且除了抽象基类,每个类都算是接口
- 受保护的属性和私有属性不在接口中:即便“受保护的”属性 也只是采用命名约定实现的(单个前导下划线);私有属性可以轻松地 访问(参见 9.7 节),原因也是如此。不要违背这些约定。
11.2 Python喜欢序列
Python数据模型的哲学是尽量支持基本协议。对序列来说,即便是最简 单的实现,Python 也会力求做到最好。
图 11-1 展示了定义为抽象基类的 Sequence 正式接口。
?.png
图 11-1:Sequence 抽象基类和 collections.abc 中相关抽象类的 UML 类图,箭头由子类指向超类,以斜体显示的是抽象方法
示例 11-3 定义 getitem 方法,只实现序列协议的一部分, 这样足够访问元素、迭代和使用 in 运算符了:
from typing import overload, _T, Iterable
class Foo:
def __getitem__(self, pos):
return range(0, 30, 10)[pos]
f = Foo()
f[1]
10
for i in f:
print(i)
0
10
20
20 in f
True
print('测试此外,使用 isinstance 和 issubclass 测试抽象基类更为人接受。=> 是否适用于鸭子类型')
import collections
print(isinstance(f, collections.MutableSequence))
print(issubclass(Foo, collections.MutableSequence))
测试此外,使用 isinstance 和 issubclass 测试抽象基类更为人接受。=> 是否适用于鸭子类型
False
False
显然,上面的是鸭子类型
虽然没有 iter 方法,但是 Foo 实例是可迭代的对象,因为发现有 getitem 方法时,Python 会调用它,传入从 0 开始的整数索引, 尝试迭代对象(这是一种后备机制)。
尽管没有实现 contains 方 法,但是 Python 足够智能,能迭代 Foo 实例,因此也能使用 in 运算 符:Python 会做全面检查,看看有没有指定的元素。
综上,鉴于序列协议的重要性,如果没有 iter 和 contains 方法,Python 会调用 getitem 方法,设法让迭代和 in 运算符可 用。
11.3 使用猴子补丁在运行时实现协议
如下, (代码与示例 1-1 相 同)
示例 11-4 实现序列协议的 FrenchDeck 类
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]
示例 11-5 random.shuffle 函数不能打乱 FrenchDeck 实例
from random import shuffle
deck = FrenchDeck()
shuffle(deck)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-2d8d99c4f0db> in <module>
1 from random import shuffle
2 deck = FrenchDeck()
----> 3 shuffle(deck)
4
D:DevInstallProjectsMiniconda3lib
andom.py in shuffle(self, x, random)
305 # pick an element in x[:i+1] with which to exchange x[i]
306 j = randbelow(i+1)
--> 307 x[i], x[j] = x[j], x[i]
308 else:
309 _int = int
TypeError: 'FrenchDeck' object does not support item assignment
错误消息相当明确,“'FrenchDeck' object does not support item assignment”('FrenchDeck' 对象不支持为元素赋值)。
这个问题的原因是,shuffle 函数要调换集合中元素的位置,而 FrenchDeck 只实现 了不可变的序列协议。
可变的序列还必须提供 setitem 方法。
Python 是动态语言,因此我们可以在运行时修正这个问题,甚至还可以 在交互式控制台中。
示例 11-6 为FrenchDeck 打猴子补丁,把它变成可变的,让 random.shuffle 函数能处理
def set_card(deck, position, card):
deck._cards[position] = card
FrenchDeck.__setitem__ = set_card
shuffle(deck)
deck[0:5]
[Card(rank='5', suit='clubs'),
Card(rank='2', suit='diamonds'),
Card(rank='6', suit='spades'),
Card(rank='8', suit='diamonds'),
Card(rank='K', suit='hearts')]
11.4 Alex Martelli的水禽
讲了几个故事,略
要点
- 使用 isinstance 和 issubclass 测试抽象基类
11.5 定义抽象基类的子类
在示例 11-8 中,我们明确把 FrenchDeck2 声明为 collections.MutableSequence 的子类:
- 为了支持洗牌,只需实现 setitem 方法。
- 是继承 MutableSequence 的类必须实现 delitem 方法,这是MutableSequence 类的一个抽象方法
- 此外,还要实现 insert 方法,这是 MutableSequence 类的第三个抽象方法
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck(collections.MutableSequence):
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]
def insert(self, position: int, value: object)-> None:
self._cards.insert(position, value)
def __setitem__(self, position: int, value: object) -> None:
self._cards[position] = value
# def __delitem__(self, position: int)-> None:
# del self._cards[position]
f = FrenchDeck()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-19-63de8e231a57> in <module>
----> 1 f = FrenchDeck()
2
3
TypeError: Can't instantiate abstract class FrenchDeck with abstract methods __delitem__
导入时(加载并编译 frenchdeck2.py 模块时),Python 不会检查抽象方 法的实现,在运行时实例化 FrenchDeck2 类时才会真正检查。因此, 如果没有正确实现某个抽象方法,Python 会抛出 TypeError 异常,如上。
FrenchDeck继承的抽象基类collections.MutableSequence的抽象方法有:
@abstractmethod
def __setitem__(self, index, value):
raise IndexError
@abstractmethod
def __delitem__(self, index):
raise IndexError
@abstractmethod
def insert(self, index, value):
'S.insert(index, value) -- insert value before index'
raise IndexError
11.6.1 collections.abc模块中的抽象基类
Iterable、Container 和 Sized
- 各个集合应该继承这三个抽象基类,或者至少实现兼容的协 议。Iterable 通过 iter 方法支持迭代,Container 通过 contains 方法支持 in 运算符,Sized 通过 len 方法支持 len() 函数。
Sequence、Mapping 和 Set
- 这三个是主要的不可变集合类型,而且各自都有可变的子类;如可变序列MutableSequence、可变Set(MutableSet)等
MappingView
- MappingView 在 Python 3 中,映射方法 .items()、.keys() 和 .values() 返回 的对象分别是 ItemsView、KeysView 和 ValuesView 的实例。前两个 类还从 Set 类继承了丰富的接口。
Callable 和 Hashable
- 这两个抽象基类与集合没有太大的关系,只不过因为 collections.abc 是标准库中定义抽象基类的第一个模块,而它们又 太重要了,因此才把它们放到 collections.abc 模块中。
- 我(指作者)从未见过 Callable 或 Hashable 的子类。
- 这两个抽象基类的主要作用是为内置 函数 isinstance 提供支持,以一种安全的方式判断对象能不能调用或 散列。
Iterator
- 注意它是 Iterable 的子类。
11.6.2 抽象基类的数字塔
numbers包定义的是“数 字塔”(即各个抽象基类的层次结构是线性的),其中 Number 是位于 最顶端的超类,随后是 Complex 子类,依次往下,最底端是 Integral 类:
- Number
- 如果想检查一个数是不是整数,可以使用 isinstance(x, numbers.Integral),这样代码就能接受 int、bool(int 的子 类),或者外部库使用 numbers 抽象基类注册的其他类型。
- Complex
- Real
- 与之类似,如果一个值可能是浮点数类型,可以使用 isinstance(x, numbers.Real) 检查。这样代码就能接受 bool、int、float、fractions.Fraction,或者外部库(如 NumPy,它做了相应的注册)提供的非复数类型。
- Rational
- Integral
11.7 定义并使用一个抽象基类
使用@abc.abstractmethod注解,可以强列要求该类的这个抽象方法derived from it。
import abc
class Tombola(abc.ABC):
@abc.abstractmethod # 强列要求该类的方法derived from it
def load(self, iterable):
"""从可迭代对象中添加元素。"""
11.7.1 抽象基类句法详解
声明抽象基类最简单的方式是继承 abc.ABC(Python 3.4 新增的类) 或其他抽象基类。
低于Python 3.4的版本,需要在 class 语句中使 用 metaclass= 关键字,把值设为 abc.ABCMeta(不是 abc.ABC):
class Tombola(metaclass=abc.ABCMeta):
# ...
metaclass= 关键字参数是 Python 3 引入的。
在 Python 2 中必须使用 metaclass 类属性:
class Tombola(object): # 这是Python 2!!!
__metaclass__ = abc.ABCMeta
# ...
11.7.3 Tombola的虚拟子类
注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之 后,注册的类会变成抽象基类的虚拟子类,而且 issubclass 和 isinstance 等函数都能识别,但是注册的类不会从抽象基类中继承任 何方法或属性。
虚拟子类不会继承注册的抽象基类,而且任何时候都不会检 查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了 避免运行时错误,虚拟子类要实现所需的全部方法。
print('实现一个简单的虚拟子类 -- ')
import abc
class Father(abc.ABC):
@abc.abstractmethod # 强列要求该类的方法derived from it
def load(self, iterable):
"""从可迭代对象中添加元素。"""
@Father.register
class Son():
pass
实现一个简单的虚拟子类 --
虚拟子类需要注意的地方
如果是 Python 3.3 或之前的版本,不能把 .register 当作类装饰器 使用,必须使用标准的调用句法。
注册之后,可以使用 issubclass 和 isinstance 函数判断 TomboList 是不是 Tombola 的子类:
issubclass(Son, Father)
True
son = Son()
isinstance(son, Father)
True
print('重新一次运行时错误: ')
son.load()
重新一次运行时错误:
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-30-1ad84f9b17cd> in <module>
1 print('重新一次运行时错误: ')
----> 2 son.load()
3
4
AttributeError: 'Son' object has no attribute 'load'
类的继承关系在一个特殊的类属性中指定: __mro__
然而,类的继承关系在一个特殊的类属性中指定—— __mro__,即方法 解析顺序(Method Resolution Order)。
Son.__mro__
(__main__.Son, object)
Father.__mro__
(__main__.Father, abc.ABC, object)
Father.__subclasses__()
[]
11.9 Python使用register的方式
虽然现在(python3.4及以后)可以把 register 当作装饰器使用了,但更常见的做法还是把 它当作函数使用,用于注册其他地方定义的类。
。例如,在 collections.abc 模块的源码中 (https://hg.python.org/cpython/file/3.4/Lib/_collections_abc.py),是这样 把内置类型 tuple、str、range 和 memoryview 注册为 Sequence 的 虚拟子类的:
Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
其他几个内置类型在 _collections_abc.py 文件 (https://hg.python.org/cpython/file/3.4/Lib/_collections_abc.py)中注册为 抽象基类的虚拟子类。
这些类型在导入模块时注册,这样做是可以的, 因为必须导入才能使用抽象基类:能访问 MutableMapping 才能编写 isinstance(my_dict, MutableMapping)。
无法理解!!!!