看一个序列协议的例子
""" 定义__getitem__方法,只实现序列协议的一部分,
这样就足够访问元素,迭代和使用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__ 方法,但是Foo实例是可迭代的对象,因为发现有__getitem__ 方法时,Python 会调用它,传入从0开始的整数索引,尝试迭代对象(这是一种后备机制)。尽管没有实现__contains__ 方法,但是Python足够智能,能迭代Foo 实例,因此也能使用in运算符:Python 会做全面检查,看看有没有指定的元素
看一个使用序列协议实现的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]
# shuffle问题
>>> from random import shuffle
>>> l = list(range(10))
>>> shuffle(l)
>>> l
[5, 2, 9, 7, 8, 3, 1, 4, 0, 6]
>>> deck = FrenchDeck()
>>> shuffle(deck)
Traceback (most recent call last):
File "" , line 1, in <module>
File ".../python3.3/random.py", line 265, in shuffle
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment
deck不能进行shuffle。这个问题的原因是,shuffle函数要调换集合中元素的位置,而FrenchDeck只实现了不可变的序列协议。可变的序列还必须提供__setitem__ 方法
>>> def set_card(deck, position, card):
... deck._cards[position] = card
...
>>> FrenchDeck.__setitem__ = set_card
"""random.shuffle 函数不关
心参数的类型,只要那个对象实现了部分可变序列协议即可"""
>>> shuffle(deck)
>>> 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')]
这里的关键是,set_card 函数要知道deck 对象有一个名为_cards 的属性,而且_cards 的值必须是可变序列。然后,我们把set_card 函数赋值给特殊方法 __setitem__,从而把它依附到FrenchDeck 类上。这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。
猴子补丁很强大,但是打补丁的代码与要打补丁的程序耦合十分紧密,而且往往要处理隐藏和没有文档的部分。
抽象基类的本质就是几个特殊方法
class Struggle:
def __len__(self):
print(str(self.__class__))
return 23
from collections import abc
isinstance(Struggle(), abc.Sized)
len(Struggle()) # 23
可以看出,无需注册,abc.Sized 也能把Struggle识别为自己的子类,只要实现了特殊方法__len__ 即可
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
# 把FrenchDeck2声明为collections.MutableSequence的子类
class FrenchDeck2(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]
# support shuffle
def __setitem__(self, position, value):
self._cards[position] = value
""" 继承MutableSequence 的类必须实现__delitem__方法,
这是MutableSequence 类的一个抽象方法"""
def __delitem__(self, position):
del self._cards[position]
# 还要实现insert 方法,这是MutableSequence 类的第三个抽象方法
def insert(self, position, value):
self._cards.insert(position, value)
https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes
对各个抽象基类做了总结,说明了相互之间的关系,以及各个基类提供的抽象方法和具体方法
import abc
# 自己定义的抽象基类要继承abc.ABC
class Tombola(abc.ABC):
"""抽象方法使用@abstractmethod 装饰器标记,
而且定义体中通常只有文档字符串"""
@abc.abstractmethod
def load(self, iterable):
"""从可迭代对象中添加元素。"""
@abc.abstractmethod
def pick(self):
"""随机删除元素,然后将其返回。
如果实例为空,这个方法应该抛出`LookupError`。
"""
def loaded(self):
"""如果至少有一个元素,返回`True`,否则返回`False`。"""
return bool(self.inspect())
def inspect(self):
"""返回一个有序元组,由当前元素构成。"""
items = []
while True:
try:
items.append(self.pick())
except LookupError:
break
self.load(items)
return tuple(sorted(items))
# 不符合要求的子类无法蒙混过关
>>> from tombola import Tombola
>>> class Fake(Tombola):
... def pick(self):
... return 13
...
>>> Fake
<class '__main__.Fake'>
>>> f = Fake()
Traceback (most recent call last):
File "" , line 1, in <module>
TypeError: Can't instantiate abstract class Fake with abstract methods load
抽象基类的语法:
class MyABC(abc.ABC):
@classmethod
@abc.abstractmethod
def an_abstract_classmethod(cls, ...):
pass
定义Tombola抽象基类的子类
import random
from tombola import Tombola
"""BingoCage 实现了所需的抽象方法load 和pick,从Tombola
中继承了loaded 方法,覆盖了inspect 方法,还增加了__call__ 方法
"""
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()
"""BingoCage 从Tombola 中继承了耗时的loaded 方法和笨拙的inspect 方法。
这两个方法都可以覆盖,下面中速度更快的一行代码。
这里想表达的观点是:我们可以偷懒,直接从抽象基类中继承不是那么理想的具体方法
"""
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)
# 覆盖loaded方法
def loaded(self):
return bool(self._balls)
# 覆盖inspect方法
def inspect(self):
return tuple(sorted(self._balls))
Tmobola的虚拟子类
注册虚拟子类的方式是在抽象基类上调用register 方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且issubclass 和isinstance 等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。
这样做时,我们保证注册的类忠实地实现了抽象基类定义的接口,而Python 会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会把我们捕获。
from random import randrange
from tombola import Tombola
@Tombola.register
# 把Tombolist 注册为Tombola 的虚拟子类
class TomboList(list):
# Tombolist 扩展list
def pick(self):
if self:
position = randrange(len(self))
return self.pop(position)
else:
raise LookupError('pop from empty TomboList')
# Tombolist.load 与list.extend 一样
load = list.extend
def loaded(self):
return bool(self)
def inspect(self):
return tuple(sorted(self))
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True
然而,类的继承关系在一个特殊的类属性中指定__mro__,即方法解析顺序(Method Resolution Order)。这个属性的作用很简单,按顺序列出类及其超类,Python 会按照这个顺序搜索方法。
查看TomboList 类的_mro_ 属性,你会发现它只列出了“真实的”超类,即list 和object
>>> TomboList.__mro__
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)
python使用register的方式
在collections.abc 模块的源码中, 是这样把内置类型tuple、str、range 和
memoryview 注册为Sequence 的虚拟子类的:
Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)