11.1 Python文化中的接口和协议
协议被定义为非正式接口,是让Python这种动态类型语言实现多态的方式
11.2 Python喜欢序列
图 11-1展示了定义为抽象基类的Sequence正式接口
示例11-3 定义__getitme__方法,只实现了序列协议的一部分,这样足够访问元素、迭代和使用in运算符了。
>>> 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
>>> 15 in f
False
复制代码
鉴于序列协议的重要性,如果没有__iter__和__contains__方法,Python会调用__getitem__方法,设法让迭代和in运算符可用。
11.3 使用猴子补丁在运行时实现协议
标注库中的random.shuffle函数用法如下:
>>> from random import shuffle
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> shuffle(l)
>>> l
[4, 6, 8, 0, 3, 5, 1, 2, 7, 9]
复制代码
示例11-5 random.shuffle函数不能打乱FrenchDeck实例
from frenchdeck import FrenchDeck
from random import shuffle
deck = FrenchDeck()
shuffle(deck)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File
...
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment
复制代码
这个报错的原因是,shuffle函数要调换collection中元素的位置,而FrenchDeck只实现了不可变序列的协议。可变的序列还必须提供__setitem__方法。
示例11-6 为FrenchDeck打猴子补丁,把它变成可变的
def set_card(deck, position, card):
deck._cards[position] = card
FrenchDeck.__setitem__ = set_card
shuffle(deck)
deck[:5]
[Card(rank='10', suit='diamonds'), Card(rank='8', suit='clubs'), Card(rank='3', suit='spades'), Card(rank='Q', suit='diamonds'), Card(rank='6', suit='diamonds')]
复制代码
猴子补丁:在运行时修改类或模块,而不改动源码。
11.4 Alex Martelli的水禽
11.5 定义抽象基类的子类
示例11-8 FrenchDeck2,collections.MutableSequence的子类
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck2(collections.MutableSequence):
ranks = [str(n) for n in (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 __setitem__(self, position, value):
self._cards[position] = value
def __delitem__(self, position):
del self._cards[position]
def insert(self, index: int, value) -> None:
self._cards[index] = value
复制代码
insert这个抽象方法是必须实现的,导入时,Python不会检查抽象方法的实现,在运行时实例化FrenchDeck2类时才会真正检查。
from frenchdeck2 import FrenchDeck2
deck = FrenchDeck2()
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class FrenchDeck2 with abstract methods insert
复制代码
图11-2 MutableSequence抽象基类
11.6 标准库中的抽象基类
11.6.1 collections.abc模块中的抽象基类
11.6.2 抽象基类的数字塔
numbers包定义的是“数字塔”(即各个抽象基类的层次结构是线性的),其中Number是位于最顶端的超类,随后是Complex子类,依次往下,最底端是Integral类:
- Number
- Complex
- Real
- Rational
- Integral
如果想检查一个数是不是整数,可以使用isinstance(x, numbers.Ingegral),这样的代码就能接受int、bool。
如果一个值可能是浮点数类型,可以使用isinstance(x, numbers.Real)检查。这样的代码能接受bool、int、float、fractions.Fraction。
11.7 定义并使用一个抽象基类
示例11-9 tombola.py: Tombola是抽象基类,有两个抽象方法和两个具体方法
import abc
class Tombola(abc.ABC):
@abc.abstractmethod
def load(self, iterable):
"""Add items from an iterable."""
@abc.abstractmethod
def pick(self):
"""Remove item at random, returning it.
This method should raise `LookupError` when the instance is empty.
"""
def loaded(self):
"""Return `True` if there's at least 1 item, `False` otherwise."""
return bool(self.inspect)
def inspect(self):
"""Return a sorted tuple with the items currently inside."""
items = []
while True:
try:
items.append(self.pick())
except LookupError:
break
self.load(items)
return tuple(sorted(items))
复制代码
示例11-11 不符合Tombola要求的子类无法蒙混过关
from tombola import Tombola
class Fake(Tombola):
def pick(self):
return 13
Fake
<class '__main__.Fake'>
f = Fake()
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class Fake with abstract methods load
复制代码
尝试实例化Fake时抛出了TypeError。Python热舞Fake是抽象类,因为它没有实现load方法。
11.7.1 抽象基类语法详解
11.7.2 定义Tombola抽象基类的子类
示例11-12 bingo.py:BingoCage是Tombola的具体子类
import random
from tombola import Tombola
class BingoCage(Tombola):
def __init__(self, items):
self._randomizer = random.SystemRandom()
self._items = []
self.load(items)
def load(self, items):
self._items.extend(items)
self._randomizer.shuffle(self._items)
def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')
def __call__(self):
self.pick()
复制代码
示例11-13 lotto.py:LotteryBlower是Tombola的具体子类,覆盖了继承的inspect和loaded方法
import random
from tombola import Tombola
class LotteryBlower(Tombola):
def __init__(self, iterable):
self._balls = list(iterable)
def load(self, iterable):
self._balls.extend(iterable)
def pick(self):
try:
position = random.randrange(len(self._balls))
except ValueError:
raise LookupError('pick from empty LotteryBlower')
return self._balls.pop(position)
def loaded(self):
return bool(self._balls)
def inspect(self):
return tuple(sorted(self._balls))
复制代码
11.7.3 Tombola的虚拟子类
即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我保证注册的类忠实地实现了抽象基类定义的接口,而Python会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会被我们捕获。
注册虚拟子类的方式是在抽象基类上调用register方法。这样做之后,注册的类会变成抽象基类的虚拟子类,而且issubclass和isinstance等函数都能识别,但是注册的类不会从抽象基类中继承任何方法和属性。
示例11-14 tombolist.py:TomboList是Tombola的虚拟子类
from random import randrange
from tombola import Tombola
@Tombola.register
class TomboList(list):
def pick(self):
if self: # 从list中继承\_\_bool__方法,列表不为空时返回True
position = randrange(len(self))
return self.pop(position)
else:
raise LookupError('pop from empty TomboList')
load = list.extend
def loaded(self):
return bool(self)
def inspect(self):
return tuple(sorted(self))
复制代码
>>> from tombola import Tombola
>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True
复制代码
类的继承关系在一个特殊的类属性中指定————__mro__,即方法解析顺序(Method Resolution Order).这个属性的作用很简单,按顺序列出类及超类,Python会按照这个顺序搜索方法。查看TomboList类的__mro__属性,会发现它只列出了“真实的”超类。
>>> TomboList.__mro__
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)
复制代码
TomboList.__mro中没有Tombola,因此TomboList没有从Tombola中继承任何方法。
11.8 Tombola子类的测试方法
__subclasses__()
这个方法返回类的直接子类列表,不包含虚拟子类。
__abc_register
只有抽象基类有这个数据属性,其值是一个WeakSet对象,即抽象类注册的虚拟子类的弱引用。
11.9 Python使用register的方式
虽然现在可以把register当作装饰器使用了,但更常见的做法还是把它当作函数使用,用于注册其他地方定义的类。
把内置类型tuple、str、range和memoryview注册为Sequence的虚拟子类:
Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
复制代码
11.10 鹅的行为有可能像鸭子
即便不注册,抽象基类也能把一个类识别为虚拟子类。
>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
复制代码
Struggle是abc.Sized的子类,这是因为abc.Sized实现了一个特殊的类方法,名为__subclasshook__.