本章的notebook文件在这里

接口:从协议到抽象基类

前面讨论过了以鸭子类型的代表,特征动态协议,本章继续来讨论到使接口更加明确的,能验证实现是否符合规定的抽象基类(Abstract Base Class, ABC).
简单来说,我们接口的限制从协议的弱限制,到抽象基类的强限制。

Python中的接口和协议

所谓接口就是类实现或者继承的公开属性,即其他对象能够调用/访问的部分。 那么按照这个定义,受保护的属性和私有属性都不属于接口。

协议则是只由文档和约定定义的非正式接口,那么它不能像正式接口那样施加限制,那么一个类可能只实现了部分接口。

一般对于Python程序员而言, “XX类对象”,“XX协议”和“XX接口”都是一个意思。

猴子补丁

所谓猴子补丁指的是在运行时对类进行修改以实现协议,例如,我们自己定义一个序列类型,但是不实现它的__getitem__方法。

1
2
3
4
class Foo:

def __init__(self, loo=[]):
self._content = loo

1
2
f = Foo([1,2,3])
f[0]

TypeError                                 Traceback (most recent call last)

<ipython-input-3-a6262614f715> in <module>()
      1 f = Foo([1,2,3])
----> 2 f[0]


TypeError: 'Foo' object does not support indexing

此时运行上面的代码会出现问题,这是可以动态实现协议给该类打上猴子补丁:

1
2
3
4
5
6
7
def set_content(foo, position):
return foo._content[position]

Foo.__getitem__ = set_content

f = Foo([1,2,3])
f[0]
1

猴子补丁固然强大,但是使用时应该注意打补丁的代码与要打补丁的程序耦合要紧密。

抽象基类

在介绍抽象基类前,谨记不要滥用抽象基类。事实上,除了对于不超过1%的高级Python程序员外,都没有必要自己定义抽象基类。因为这么做往往会表明语言太注重表面形式且有巨大风险,只要正确使用现有的抽象基类就能获得99.99%的好处。

collections.abc

Python3.4在collections.abc模块中定义两个16个抽象基类,可以分成几类:

  • Iterable、Container 和 Sized   
    各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable 通过 iter 方法支持迭代,Container 通过 contains 方法支持 in 运算符,Sized 通过 len 方法支持 len() 函数。
  • Sequence、Mapping 和 Set   这三个是主要的不可变集合类型,而且各自都有可变的子类。MutableSequence 的 详细类图见图 11-2;MutableMapping 和 MutableSet 的类图在第 3 章中。
  • MappingView   在 Python 3 中,映射方法 .items()、.keys() 和 .values() 返回的对象分别是 ItemsView、KeysView 和 ValuesView 的实例。前两个类还从 Set 类继承了丰富的接口。
  • Callable 和 Hashable   这两个抽象基类与集合没有太大的关系,只不过因为 collections.abc 是标准库中 定义抽象基类的第一个模块,而它们又太重要了,因此才把它们放到 collections.abc 模块中。我从未见过 Callable 或 Hashable 的子类。这两个抽象基类的主要作用是为内 置函数 isinstance 提供支持,以一种安全的方式判断对象能不能调用或散列。
  • Iterator   注意它是 Iterable 的子类。

抽象基类的数字塔

numbers包定义的是数字塔,即各个抽象基类的层次结构是线性的,其中Number是位于最顶端的超类,随后是Complex子类,依次往下,最底端是Intergral类。

  1. Number
  2. Complex
  3. Real
  4. Rational
  5. Integral

因此可以用isinstance(x, numbers.Integral)来检查一个数是不是整数的.其他类型同理.

定义并使用一个抽象基类

下面我们实现并演示一个抽象基类的使用,以此来说明如何阅读标准库/其他包中的源码,而不是鼓励每个人都定义抽象基类.

这里我们假设一个场景,即需要在网站上显示随机广告,在每个广告都显示一遍之前,不会重复显示广告.我们将他命名为Tombola,它有4个方法:

两个抽象方法:

  • .load(…):把元素放入容器。
  • .pick():从容器中随机拿出一个元素,返回选中的元素。

另外两个是具体方法:

  • .loaded():如果容器中至少有一个元素,返回 True。
  • .inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容 (内部的顺序不保留)。
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
import abc
class Tombola(abc.ABC):

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


注意上面代码中几点:

  • 抽象方法用@abstractmethod装饰器标记,而且定义体中通常只有文档字符串.
  • 在抽象基类的具体方法(例如inspect)中,我们不知道具体子类如何操作,因此只依赖抽象基类中定义的接口

下面我们构造一个子类来说明抽象基类的限制:

1
2
3
4
5
6
class Fake(Tombola):

def pick(self):
return 13

Fake
__main__.Fake

1
f = Fake()

TypeError                                 Traceback (most recent call last)

<ipython-input-13-eb09ed8f651b> in <module>()
----> 1 f = Fake()


TypeError: Can't instantiate abstract class Fake with abstract methods load

上面可以看到由于Fake子类不符合Tombola的要求(没有实现所有抽象方法),因此无法通过它来构造对象.

接着我们定义一个真实有效的抽象基类的子类,它满足了Tombola规定的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BingoCage(Tombola):  # <1>

def __init__(self, items):
self._randomizer = random.SystemRandom() # <2>
self._items = []
self.load(items) # <3>

def load(self, items):
self._items.extend(items)
self._randomizer.shuffle(self._items) # <4>

def pick(self): # <5>
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')

def __call__(self): # <7>
self.pick()

下面是另一个子类,在这个子类中,除了实现了抽象方法,也对原来Tombola中的具体方法做了覆盖来适应子类的情况从而实现了运行速度的提高.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LotteryBlower(Tombola):

def __init__(self, iterable):
self._balls = list(iterable) # <1>

def load(self, iterable):
self._balls.extend(iterable)

def pick(self):
try:
position = random.randrange(len(self._balls)) # <2>
except ValueError:
raise LookupError('pick from empty BingoCage')
return self._balls.pop(position) # <3>

def loaded(self): # <4>
return bool(self._balls)

def inspect(self): # <5>
return tuple(sorted(self._balls))

虚拟子类

注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之后,注册的类会变成 抽象基类的虚拟子类,而且 issubclassisinstance 等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性.

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Tombola.register  # <1>
class TomboList(list): # <2>

def pick(self):
if self: # <3>
position = randrange(len(self))
return self.pop(position) # <4>
else:
raise LookupError('pop from empty TomboList')

load = list.extend # <5>

def loaded(self):
return bool(self) # <6>

def inspect(self):
return tuple(sorted(self))
1
issubclass(TomboList, Tombola)
True
1
2
t = TomboList(range(100))
isinstance(t, Tombola)
True

类的继承关系会在一个特殊的类属性中指定__mro__(Method Resolution Order),通过该方法可以发现TomboList只列出了真实的超类:

1
TomboList.__mro__
(__main__.TomboList, list, object)

上面可以发现其中没有Tombola,因此Tombolist没有从Tombola中继承任何方法.

杂谈

本章最后的杂谈中也有一些有趣的话题:

  • Python是一个动态强类型语言.
    1. 如果一门语言很少隐式转换类型,说明它是强类型语言;否则说明它是若类型语言.Java,C++,Python都是强类型语言,PHP,Js,Perl都是弱类型语言.
    2. 编译时检查类型的语言是静态类型语言,运行时检查类型的是动态类型的语言.静态语言往往需要声明类型