接口:从协议到抽象基类

抽象类表示接口。                    ---------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 <module>
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, in <module>
 9     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)

 

最后说一点:不要自己定义抽象基类,而应该创建现有基类的子类,或者注册虚拟子类。

posted on 2019-03-06 11:24  forwardFields  阅读(399)  评论(0编辑  收藏  举报

导航