抽象类表示接口。 ---------Bjarne StroustrupC++之父
Python中的接口和协议
引入抽象基类之前,python就已经很成功了,即便现在也很少有代码使用抽象基类。协议可以看成是非正式的接口,是python这种动态语言实现动态的方式。
接口在动态语言中是如何运作的呢?首先,python中除了抽象基类,每个类都有接口:类实现和公开的属性,包括特殊方法如__getitem__, __iter__等。
关于接口,接口是实现特定角色的方法的集合。一个类可能会实现多个接口,从而让实例扮演多种角色。
协议是接口,但不是正式的,一个类允许只实现部分接口。有时,有些API只要求对象拥有.read()方法即可。
序列协议是pyhton最基础的协议之一。即便只实现了那个协议最基础的一部分,解释器也会对其进行处理。
Python喜欢序列
python数据模型的哲学是尽量支持基本协议。对序列模型来说,即便是最简单的实现,python也力求做到最好。
序列Sequence的抽象基类接口:
我们自己实现一个Foo类,它并不继承abc.Sequence,而是实现序列协议的一个方法__getitem__。
1 class Foo: 2 def __getitem__(self, index): 3 return (range(30))[index] 4 5 foo = Foo() 6 for val in foo: 7 print(val) 8 print(foo[2]) 9 print(foo[-1]) 10 print(-7 in foo)
虽然我们并没有实现__iter__和__contains__方法,但是foo却是一个可迭代的对象,为什么呢?
我们定义了__getitem__方法,python会调用它,从下标为0开始,尝试着迭代对象(这属于python的一种后备机制)。同理,尽管并没有__contains__方法,但是仍然可以使用in运算符来遍历对象查找指定元素是否存在。
也就是说,如果没有__iter__和__contains__方法,python会退而求其次调用__getitem__方法,设法让迭代和in运算符可用。
1 class FrenchDeck: 2 ranks = [str(n) for n in range(2, 11)] + list('JQKA') 3 suits = 'spades diamonds clubs hearts'.split() 4 def __init__(self): 5 self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits] 6 7 def __len__(self): 8 return len(self._cards) 9 10 def __getitem__(self, index): 11 return self._cards[index] 12 13 deck = FrenchDeck() 14 print(len(deck)) 15 print(deck[2])
使用猴子补丁在运行时实现协议
FrenchDeck有个缺陷就是无法洗牌,random模块中有个shuffle方法。python文档对齐描述是就地打乱列表顺序。
1 import random 2 l = list(range(30)) 3 print(l) 4 random.shuffle(l) 5 print(l)
将shuffle应用在deck上:
解释器报错,根据异常信息可以知道,FrenchDeck不支持元素赋值,那就手动添加一下吧。
方法一:在类内实现__setitem__方法
1 class FrenchDeck: 2 ranks = [str(n) for n in range(2, 11)] + list('JQKA') 3 suits = 'spades diamonds clubs hearts'.split() 4 def __init__(self): 5 self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits] 6 7 def __len__(self): 8 return len(self._cards) 9 10 def __getitem__(self, index): 11 return self._cards[index] 12 13 def __setitem__(self, index, val): 14 self._cards[index] = val 15 16 deck = FrenchDeck() 17 print(len(deck)) 18 print(deck[2]) 19 20 for card in deck: 21 print(card) 22 23 random.shuffle(deck) 24 for card in deck: 25 print(card)
方法二:打补丁(运行时添加)
1 class FrenchDeck: 2 ranks = [str(n) for n in range(2, 11)] + list('JQKA') 3 suits = 'spades diamonds clubs hearts'.split() 4 def __init__(self): 5 self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits] 6 7 def __len__(self): 8 return len(self._cards) 9 10 def __getitem__(self, index): 11 return self._cards[index] 12 13 #def __setitem__(self, index, val): 14 # self._cards[index] = val 15 16 def set_card(deck, index, val): 17 deck._cards[index] = val 18 deck = FrenchDeck() 19 print(len(deck)) 20 print(deck[2]) 21 22 for card in deck: 23 print(card) 24 FrenchDeck.__setitem__ = set_card 25 random.shuffle(deck) 26 for card in deck: 27 print(card)
方法二在类外部定义了set_card函数,然后设置类属性__setitem__引用set_card,__setitem__接收三个参数,第一个是对象自身,第二个是索引下标,第三个是值,参数名是无关紧要的,在类内部方法第一个参数命名成self是中python约定俗成的惯例,你命名成其他名字除了不规范之外,对程序本身没有任何影响。
1 class Foo: 2 def f(python): 3 print(python) 4 print("--------") 5 6 foo = Foo() 7 foo.f()
方法二这种技术叫猴子补丁:在运行时修改类或模块,不改动源码。
此外,还说明一点,random.shuffle不关心它的参数类型,只要参数实现了序列协议即可,这就是鸭子类型。
鸭子类型:不关心对象的具体类型,对象只需实现特定的协议即可。
什么是抽象基类
抽象基类是用于封装框架引入的一般性概念和抽象。例如“一个序列“,”一个数”。基本上不需要自己编写新的抽象基类,只要正确的使用现有的抽象基类技能获得99.9%的好处,而不用冒着设计不当导致的巨大风险。
----------Alex Martelli
在本人看来,抽象基类类似数学上的各种概念定义。
举个例子来说,数学上定义了集合:是指具有某种特定性质的具体的或抽象的对象汇总而成的集体。其中,构成集合的这些对象则称为该集合的元素。他并没有告诉你到底集合长什么样子,只是说具有某种性质的元素集体就是集合。抽象基类也是类似,他只是定义了一些性质和概念,比如一个序列应该具有怎样的性质?应该能够拥有大小、获取序列的元素等等;一个数应该有怎么的性质?应该能够比较相等性等等。
定义抽象基类的子类
定义一个子类的序列继承自collections.abc.MutableSequence。
1 class Foo(collections.abc.MutableSequence): 2 pass 3 4 foo = Foo()
一运行,唔:
File "E:\test.py", line 909, in
foo = Foo()
TypeError: Can't instantiate abstract class Foo with abstract methods __delitem__, __getitem__, __len__, __setitem__, insert
错误提示说,无法实例化带有抽象方法__delitem__,__getitem__,__len__,__setitem__,insert的Foo类对象。即是说我们必须要自己实现这几个抽象方法。
以FrenchDeck为例。
1 import collections 2 class FrenchDeck(collections.abc.MutableSequence): 3 ranks = [str(n) for n in range(2, 11)] + list('JQKA') 4 suits = 'spades diamonds clubs hearts'.split() 5 def __init__(self): 6 self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits] 7 8 def __len__(self): 9 return len(self._cards) 10 11 def __getitem__(self, index): 12 return self._cards[index] 13 14 def __setitem__(self, index, val): 15 self._cards[index] = val 16 17 def __delitem__(self, index): 18 del self._cards[index] 19 20 def insert(self, index, val): 21 self._cards.insert(index, val) 22 23 24 deck = FrenchDeck() 25 print(len(deck)) 26 print(deck[2]) 27 28 for card in deck: 29 print(card) 30 31 random.shuffle(deck) 32 for card in deck: 33 print(card)
Alex说的,“抽象基类就是几个特殊方法”就是这个意思。
MutableSequence抽象基类的继承关系UML图,箭头由子类指向父类,斜体表示的是抽象基类和抽象方法
从图中就可以看出,FrenchDeck继承自MutabeSequence,而MutableSequence中的抽象方法有__setitem__, __delitem__, insert, __len__,__getitem__,这也正是我们要自己实现的。而其他方法则可以拿来即用,因为有些抽象方法和类方法如__contains__,__iter__, __reversed__, index, count已经在MutableSequence的父类Sequence实现了,MutableSequence也实现了一些方法。
1 class FrenchDeck(collections.abc.MutableSequence): 2 ranks = [str(n) for n in range(2, 11)] + list('JQKA') 3 suits = 'spades diamonds clubs hearts'.split() 4 def __init__(self): 5 self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits] 6 7 def __len__(self): 8 return len(self._cards) 9 10 def __getitem__(self, index): 11 return self._cards[index] 12 13 def __setitem__(self, index, val): 14 self._cards[index] = val 15 16 def __delitem__(self, index): 17 del self._cards[index] 18 19 def insert(self, index, val): 20 self._cards.insert(index, val) 21 22 23 deck = FrenchDeck() 24 print(iter(deck)) 25 for card in deck: 26 print(card) 27 for card in reversed(deck): 28 print(card) 29 print(deck.index(Card('10', 'hearts'))) 30 print(deck.count(Card('10', 'hearts'))) 31 32 deck.append(Card('Jocker', 'little')) 33 print(deck[-1]) 34 deck.pop() 35 print(deck[-1]) 36 deck.extend([Card('Jocker', 'little'), Card('Jocker', 'big')]) 37 print(deck[-1], deck[-2]) 38 deck.remove(Card('Jocker', 'big')) 39 print(deck[-1]) 40 print(Card('J', 'diamonds') in deck) 41 for card in deck: 42 print(card)
ps:也可以通过覆盖的方法,重写一些函数。比如若自定义的序列类型是有序的,那就可以覆盖__contains__方法,使用bisect二分查找,提高效率。
标准库中的抽象基类
python中的大多数抽象基类都在collections.abc模块中定义。number和io包中也有一些。不过还是collections.abc中的抽象基类比较常用。
collections.abc中的抽象基类
说明:
标准库中有两个名为abc的模块,一个是collections.abc;另一个就是abc,它定义的是abc.ABC类。每个抽象基类都依赖这个类,但不用导入它,除非自定义新的抽象基类。
collections.abc模块中各个抽象基类的UML类图
看的眼花缭乱?没关系,下面来理一理这些基类。
Iterable、Container、和Sized
所有的类除了MappingView都继承了这三个抽象基类或者实现相应的协议。Iterable通过__iter__方法支持迭代,Container通过__contains__方法支持in运算符,Sized通过__len__方法支持len(obj)函数。
Sequence、Mapping、Set
这三个是不可变集合类型,而且用于可变的子类型,list、dict、set就分别是它们的子类。
MappingView
映射方法.items()、.keys()、.values()返回的对象分别是KeysView和ValuesView的实例。
Callable、Hashable
这两个抽象基类跟集合没什么太大的联系。它们通常被isinstance函数使用,用来判断对象是否是可调用的或者是可哈希的。
Iterator
Iterator迭代器,是Iterable的子类。
除了collections.abc,最常用的抽象基类包就是numbers。
抽象基类的数字塔
numbers包定义的是“数字塔”,其中Number位于最顶端,往下是Complex,最底端是Intergral类。
* Number
* Complex
* Real
* Rational
* Integral
比如,检测x是不是整数可以用isinstance(x, numbers.Integral),检测是不是浮点数可以用isinstance(x, numbers.Real)。
下面我们开始实现一个抽象基类。
ps:书中并不鼓励用户自定义抽象类型,只是帮助阅读标准库和其他包内的抽象基类源码。
定义并使用抽象基类
我们定义一个抽象基类命名为Tombola,这是宾果机和打乱数字的滚动容器的意大利名。
Tombola抽象基类有4个方法,有2个抽象方法。
* .load(...):把元素放进容器
* .pick(...):随机取出一个元素
还有2个具体方法:
* .loaded():如果容器里有元素,fanhuiTrue;否则返回False
* .inspect():返回一个有序元祖,由容器里的现有元素构成,但不会修改容器的内容(内部的顺序不保留)
自己定义Tombola抽象基类
1 import abc 2 import random 3 class Tombola(metaclass=abc.ABCMeta): 4 @abstractmethod 5 def load(self, elem): 6 """ 7 把元素放入容器中 8 """ 9 10 @abstractmethod 11 def pick(self): 12 """ 13 从容器中取出元素,如果容器为空,抛出StopIteration异常 14 """ 15 16 def loaded(self): 17 return bool(self.inspect()) 18 19 def inspect(self): 20 elems = [] 21 while True: 22 try: 23 elems.append(self.pick()) 24 except StopIteration: 25 break 26 for elem in elems: 27 self.load(elem) 28 return tuple(sorted(elems))
书上展示了Tombola抽象基类和三个具体实现。
1 import abc 2 class Tombola(abc.ABC): 3 @abc.abstractmethod 4 def load(self, iterable): 5 """ 6 从可迭代对象中添加元素 7 """ 8 @abc.abstractmethod 9 def pick(self): 10 """ 11 随机删除一个元素 12 如果实例为空,抛出LookupError 13 """ 14 15 def loaded(self): 16 """ 17 至少有一个元素返回True,否则返回False 18 """ 19 return bool(self) 20 21 def inspect(self): 22 """ 23 返回一个有序元祖,由当前元素构成 24 """ 25 items = [] 26 while True: 27 try: 28 items.append(self.pick()) 29 except LookupError: 30 break 31 self.load(items) 32 return tuple(sorted(items))
* 自己定义的抽象基类要继承abc.ABC
* 抽象方法使用@abc.abstractmethod装饰器标记,而且函数体通常只有文档字符串
* 根据文档字符串描述,如果没有元素可选,应该抛出LookupError异常
* 抽象基类中也可以包含具体方法
* 虽然不知道子类如何存储元素,但是可以调用pick方法,获取inspect的结果
* 最后不要忘了把inspect结果重新赋值回去
子类务必要实现抽象基类的抽象方法,不然无法实例化对象。
1 class Foo(Tombola): 2 pass 3 4 foo = Foo() 5 6 """ 7 运行结果 8 File "E:\test.py", line 983, in9 foo = Foo() 10 TypeError: Can't instantiate abstract class Foo with abstract methods load, pick 11 """
一些注意点:
* abc.ABC是python3.4新增的类,如果是旧版python,那就应该像笔者自己定义的那也,使用metaclass=abc.ABCMeta。
* 抽象基类的inspect()方法实现的有些复杂,但是表明了一件事:抽象基类可以实现具体方法,只要依赖接口的其他方法就行。Tombola的子类根据具体的数据结构,可以覆盖父类的inspect()方法,使用更加高效的方式实现。
* loaded()方法同理
* 书上之所以捕获LookupError异常,是因为子类可能抛出KeyError或者IndexError,但是在Python的异常结构中,无论是IndexError还是KeyError都是LookupError的子类,所以使用LookupError可以将LookupError本身及其子类"一网打尽"。
抽象基类语法
python3.4或者更高版本:直接继承abc.ABC
python3:class语句中使用metaclass=abc.ABCMeta
python2:使用__metaclass__ = abc.ABCMeta
import abc #python3.4及以上 class Foo(abc.ABC): ... #python3 class Foo(metaclass=abc.ABCMeta): ... #python2 class Foo(object): __metaclass__ = abc.ABCMeta
除了@abstractmethod之外,abc还提提供了@abstractclassmethod、@abstractstaticmethod、@abstractproperty三个装饰器,但是后三个从python3.3开始被删除了,因为装饰器可以叠加在@abstractmethod上,例如声明抽象类方法:
1 class MyABC(abc.ABC): 2 3 @classmethod 4 @abc.abstractmethod 5 def class_abstract_method(cls, *args): 6 pass
在函数上叠加装饰器的顺序很重要,@abstractmethod的文档指出了这一点:
“与其他描述符一起使用时,abstractmethod()应该放在最里层.......”
也就是说,其他装饰器都应该放在abstractmethod上面。
定义Tombola抽象基类的具体子类
BingoCage类实现:
1 class BingoCage(Tombola): 2 def __init__(self, items): 3 self._randomizer = random.SystemRandom() 4 self._items = [] 5 self.load(items) 6 7 def load(self, items): 8 self._items.extend(items) 9 self._randomizer.shuffle(self._items) 10 11 def pick(self): 12 try: 13 return self._items.pop() 14 except IndexError: 15 raise LookupError('pick from empty BingoCage') 16 17 def __call__(self): 18 self.pick() 19 20 bingo = BingoCage(range(10)) 21 print(bingo.loaded(), bingo.inspect()) 22 print(bingo.pick()) 23 print(bingo.loaded(), bingo.inspect())
* self._randomizer是系统SystemRandom提供的随机化模块(os.urandom()),但是要注意的是,并不是在所有系统上都可用
* 子类继承了父类的loaded和inspect方法,也可以重写这两个方法
LotteryBlower类是Tombola的另一种实现:
1 class LotteryBlower(Tombola): 2 def __init__(self, iterable): 3 self._balls = list(iterable) 4 5 def load(self, iterable): 6 self._balls.extend(iterable) 7 8 def pick(self): 9 random.shuffle(self._balls) 10 try: 11 index = random.choice(range(len(self._balls))) 12 val = self._balls.pop(index) 13 except IndexError: 14 raise LookupError('pick from an empty container') 15 return val 16 17 def loaded(self): 18 return bool(self._balls) 19 20 def inspect(self): 21 return tuple(sorted(self._balls)) 22 blower = LotteryBlower(range(10)) 23 print(blower.loaded(), blower.inspect()) 24 print(blower.pick()) 25 print(blower.loaded(), blower.inspect()) 26 for i in range(10): 27 print(blower.pick()) #LookupError
LotteryBlower重写了loaded、和inspect方法,提高了效率,此外__init__方法中,self._balls保存的是list(iterable),而不是iterable的引用,这点要注意。
白鹅类型
http://en.wikipedia.org/wiki/Duck_typing#History
引用自Alex Martelli的话:
“白鹅类型指,只要cls是抽象基类,即cls的元类是abc.ABCMeta,就可以使用isinstance(obj, cls)”。
Tombola的虚拟子类
白鹅类型的一个基本特性就是:即便不使用继承,也有办法把某个类注册为抽象基类的虚拟子类。所谓虚拟子类,即不是通过继承的方式实现的子类。使用虚拟子类时,要保证已经实现了抽象基类的定义的所有借口,python不会做任何检查。
通过抽象基类.register(子类)的方式注册虚拟子类,也支持装饰器@语法某个类注册成了虚拟子类后,issubclass、isinstance都能被识别,但是子类确不会继承来自虚拟父类的任何属性或者方法。
虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽象基类的接口,即便在实例化时也不会检查。
使用装饰器语法实现的TomboList类,它是list的真实子类也是Tombola的虚拟子类。
1 @Tombola.register 2 class TomboList(list): 3 def pick(self): 4 random.shuffle(self) 5 try: 6 index = random.choice(range(len(self))) 7 val = self.pop(index) 8 except IndexError: 9 raise LookupError('pick from an empty container') 10 return val 11 12 def load(self, iterable): 13 self.extend(iterable) 14 15 def loaded(self): 16 return bool(self) 17 18 def inspect(self): 19 return tuple(self) 20 21 t = TomboList(range(10)) 22 print(t) 23 print(isinstance(t, Tombola), issubclass(TomboList, Tombola)) 24 t.load(i for i in range(10, 20)) 25 print(t) 26 print(t.pick()) 27 print(t.loaded(), t.inspect())
ps:注册虚拟子类也可以使用 TomoList = Tombola.register(TomoList)
类有一个特殊的属性----__mro__,它是按照C3算法计算出的继承的父类列表,查看TomboList的__mro__属性
可以看出并没有Tombola,因此TomboList没有从Tombola继承任何属性和方法。
Python使用register的方式
虽然可以把register当成装饰器使用,但更常见的做法还是把它当成函数,用来注册在其他地方定义的类。
例如,在collections.abc中就是用函数的方式把str、tuple等注册成Sequence的虚拟子类的:
1 Squence.register(tuple) 2 Squence.register(str) 3 Squence.register(range) 4 Squence.register(memoryview)
最后说一点:不要自己定义抽象基类,而应该创建现有基类的子类,或者注册虚拟子类。