python中的抽象基类

协议和接口

python中的协议和接口的概念不一样,接口是类似其他编程语言中一样比较严格的定义方式,而协议的概念有种约定俗成的味道。python中的有种鸭子类型的称呼,即说的就是通过协议的方式实现的行为。例如,python中的序列,如果通过抽象类的继承的方式就需要实现Sequence类下的多个抽象方法,而鸭子类型就让我们只通过实现getitem方法就能让我们的类的部分表现像序列一样,可以通过下标进行取值。

协议是一种非正式的接口,因此不能像正式接口那样施加限制。一个类可以实现多个协议,从而令类的表现像从多个继承来的一样(事实上并没有)。

抽象类

python中也可以像其他语言一样定义抽象类,不过大多时候不需要这么做,常见的做法是继承已经存在的抽象基类。

下面说明如何继承一个已经存在的抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from collections import MutableSequence, namedtuple
Card = namedtuple("Card", ["rank", "suit"])
class FrenchDeck(MutableSequence):
ranks = [str(n) for n in range(2, 11)] + list("JKQA")
suits = 'spades diamonds clubs harts'.split()
def __init__(self):
self._cards = [Card(rank, suit)
for suit in self.suits for rank in self.ranks]
def __getitem__(self, position):
return self._cards[position]
def __setitem__(self, position, value):
self._cards[position] = value
def __len__(self):
return len(self._cards)
def __delitem__(self,position):
del self._cards[position]
def insert(self,position,value):
self._cards.insert(position,value)
f = FrenchDeck()

继承抽象基类的子类必须全部实现抽象基类中的抽象方法,像例子中的getitem,__setitem,\len,\delitem__,insert这些方法是必须实现的,否则在运行过程中python会告诉你没有实现某个方法而报警:

1
2
3
4
Traceback (most recent call last):
File "pythonscripts/abc_frenchdeck.py", line 29, in <module>
f = FrenchDeck()
TypeError: Can't instantiate abstract class FrenchDeck with abstract methods __delitem__, insert

collections.abc模块中的抽象基类

标准库中的抽象基类大多数在collections.abc模块中定义,部分在numbers和io中。

常见的抽象基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Iterable
Container
Sized
Callable
Hashable
Iterator
Sequence
Mapping
Set
MappingView
MutableSequence
MutableMapping
MutableSet
ItemsView
KeysView
ValuesView

自定义抽象基类

尽管不推荐自定义抽象基类,但是在某些特殊的情况下还是有需要自己创建抽象基类的,创建抽象基类需要继承自abc.ABC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import abc
class animal(abc.ABC):
@abc.abstractmethod
def run(self):
''''''
@abc.abstractmethod
def eat(self):
''''''
@abc.abstractmethod
def shit(self):
''''''

abc.ABC是从python3.4后新增的,对于之前的版本必须使用metaclass=关键字:

1
class animal(metaclass=abc.ABCMeta):

对于python2 则只能通过metaclass来实现了:

1
2
class animal(object):
__metaclass__ = abc.ABCMeta

虚拟子类

除了继承,其实还在另外一种将类注册为抽象基类的子类的方法,通过register方法.

例如,我们实现一个dog类,但不继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@animal.register
class dog:
def run(self):
print('10km/s')
def eat(self):
print('eat meat')
def shit(self):
print('pull shit')
d = dog()
print(isinstance(d,animal))
print(issubclass(dog,animal))
---------------
True
True

register方法可以通过装饰器的方式放在子类上(对于3.3版本以前的python只能调用标准的句法),由上面的例子可以看出来,isinstance和issubclass都认可子类的这个事实。

但是在mro中,却没有列出animal,说明dog并不是从mro中继承而来的:

1
2
3
print(dog.__mro__)
-----------------
(<class '__main__.dog'>, <class 'object'>)

另外一个事实

某些情况下,即使不使用register方法,也能把一个类注册为抽象基类的子类。看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
from collections import abc
class demo:
def __len__(self):
return 3
print(isinstance(demo(),abc.Sized))
print(issubclass(demo,abc.Sized))
--------------
True
True

这里的子类只实现了一个len竟然也成了抽象基类Sized的子类,那么原理是什么呢?

这是因为Sized实现了一个特殊的方法subclasshook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Sized(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
if any("__len__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented

由Sized源码我们可以看到了我们要的答案。

虽然这为我们实现提供了另外的一种思路,但是还是不推荐这么做,因为,Sized这里是个特例,len方法的目的十分明确,而在我们自己实现过程中,恐怕是很难有这么明确的行为目的的。