深入理解Python之接口

Python文化中的接口和协议

PythonABCs引入之前已经很成功了.接口在动态语言中是如何工作的。其没有interface关键字。对于ABCs,每个类有个接口。协议是接口。接口的定义是对象公共方法的子集,能够实现特定的功能。Python中最基础的接口之一是序列协议

运行中实现协议

1
2
3
l = list(range(10))
shuffle(l)
print(l)

对于普通自定义对象,如果想使用shuffle那么需要实现__setitem__,因此可以动态设置xxx.__setitem__ = set_xxx。但这其中暴露了__setitem__给外界,破坏了封装性

ABC子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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]

def __setitem__(self, position, value):
self._cards[position] = value

def __delitem__(self, position):
del self._cards[position]

def insert(self, position, value):
self._cards.insert(position, value)

继承链是MutableSequence->Sequence

标准库中的ABCs

Python2.6之后引入了ABCs

collections.abc

  • Iterable, Container, Sized
  • Sequence, Mapping, Set
  • MappingView
  • Callable, Hashable
  • Iterator

除了collections.abc之外,标准库中最有用的ABCs就是numbers

numbers

  • Number
  • Complex
  • Real
  • Rational
  • Integral

因此我们需要使用isinstance(x, numbers.Integral)来检查整形。需要注意的是decimal.Decimal没有成为numbers.Real的子类

定义和使用ABC

假设我们需要在网页或者APP上随机展示广告。我们将定义名为Tombola的抽象类。

Tombola抽象类有四个方法。两个抽象方法是

  • load(): 放条目到容器中
  • pick(): 随机移除并返回条目

实体方法

  • loaded(): 如果容器至少有一个条目,那么返回True
  • inspect(): 返回排过序的的元组

tombola

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

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, 'Falsle` 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))

使用@abc.abstractmethod标识抽象方法

1
2
3
4
class Fake(Tombola):
def pick(self):
return 13

ABC详细语法

声明抽象类的最好方式是继承abc.ABC或者其他ABC。然而abc.ABCPython3.4才引入的。在这此前必须使用metaclass=keyword

1
2
class Tombola(metaclass=abc.ABCMeta):
# ...

metaclass=keywordPython3才引入的,Python2中,必须使用__metaclass__

1
2
3
class Tombola(object):
__metaclass__ = abc._ABCMeta

Tombola的子类

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
32
33
34
35
36
37
38
39
40

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()

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 BingoCage')
return self._balls.pop(position)

def loaded(self):
return bool(self._balls)

def inspect(self):
return tuple(sorted(self._balls))

Tombola的虚子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@Tombola.register
class TomboList(list):
def pick(self):
if self:
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))

使用@Tombola.register注册作为Tombola的虚子类

注意因为注册了,所以issubclassisinstance都能表现为TomboListTombola的子类。

但是打印继承关系

1
2
3
4
print(TomboList.__mro__)

result:
(<class '__main__.TomboList'>, <class 'list'>, <class 'object'>)

可以看出TomboList并没有集成自Tombola

Tombola子类测试

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
TEST_FILE = 'tombola_tests.rst'
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed - {2}'


def main(argv):
verbose = '-v' in argv
real_subclasses = Tombola.__subclasses__()
virtual_subclasses = list(Tombola._abc_registry)

for cls in real_subclasses + virtual_subclasses:
test(cls, verbose)


def test(cls, verbose=False):
res = doctest.testfile(
TEST_FILE,
globs={'ConcreteTombola': cls},
verbose=verbose,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)

tag = 'FAIL' if res.failed else 'OK'
print(TEST_MSG.format(cls.__name__, res, tag))


if __name__ == '__main__':
import sys

main(sys.argv)