python编写代码定义一个形状基类_fluent python 11.7节 定义并使用一个抽象基类

为了证明有必要定义抽象基类,我们要在框架中找到使用它的场景。想 象一下这个场景:你要在网站或移动应用中显示随机广告,但是在整个 广告清单轮转一遍之前,不重复显示广告。假设我们在构建一个广告管 理框架,名为 ADAM。它的职责之一是,支持用户提供随机挑选的无重复类。 为了让 ADAM 的用户明确理解“随机挑选的无重复”组件是什么 意思,我们将定义一个抽象基类。

受到“栈”和“队列”(以物体的排放方式说明抽象接口)启发,我将使用 现实世界中的物品命名这个抽象基类:宾果机和彩票机是随机从有限的 集合中挑选物品的机器,选出的物品没有重复,直到选完为止。

我们把这个抽象基类命名为 Tombola,这是宾果机和打乱数字的滚动容 器的意大利名。

Tombola 抽象基类有四个方法,其中两个是抽象方法。.load(...):把元素放入容器。

.pick():从容器中随机拿出一个元素,返回选中的元素。另外两个是具体方法。

.loaded():如果容器中至少有一个元素,返回 True。

.inspect():返回一个有序元组,由容器中的现有元素构成,不 会修改容器的内容(内部的顺序不保留)。

下图展示了 Tombola 抽象基类和三个具体实现。

Tombola

load

pick

loaded

inspect

^ ^ ^

| | `(registered)

| | `

| | `

BingoCage LotteryBlower <>

__init__ __init__ TomboList

load load load

pick pick pick

__call__ loaded loaded

inspect inspect

一个抽象基类和三个子类的 UML 类图。根据 UML 的约 定,Tombola 抽象基类和它的抽象方法使用斜体。虚线箭头用于表 示接口实现,这里它表示 TomboList 是 Tombola 的虚拟子类,因为 TomboList 是注册的,本章后面会说明这一点

Tombola 是抽象基类,有两个抽象方法和 两个具体方法

import abc

class Tombola(abc.ABC):

# 抽象方法使用 @abstractmethod 装饰器标记,而且定义体中通常只 有文档字符串。

@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 = []

# 我们不知道具体子类如何存储元素,不过为了得到 inspect 的结 果,我们可以不断调用 .pick() 方法,把 Tombola 清空……

while True:

try:

items.append(self.pick())

except LookupError:

break

# ……然后再使用 .load(...) 把所有元素放回去。

self.load(items)

return tuple(sorted(items))其实,抽象方法可以有实现代码。即便实现了,子类也必须 覆盖抽象方法,但是在子类中可以使用 super() 函数调用抽象方 法,为它添加功能,而不是从头开始实现。@abstractmethod 装 饰器的用法参见 abc 模块的文档 (https://docs.python.org/3/library/abc.html)。

上例中的 .inspect() 方法实现的方式有些笨拙,不过却表明, 有了 .pick() 和 .load(…) 方法,若想查看 Tombola 中的内容,可以 先把所有元素挑出,然后再放回去。这个示例的目的是强调抽象基类可 以提供具体方法,只要依赖接口中的其他方法就行。Tombola 的具体子 类知晓内部数据结构,可以覆盖 .inspect() 方法,使用更聪明的方式 实现,但这不是强制要求。

上例中的 .loaded() 方法没有那么笨拙,但是耗时:调用 .inspect() 方法构建有序元组的目的仅仅是在其上调用 bool() 函数。这样做是可以的,但是具体子类可以做得更好,后文见分晓。

注意,实现 .inspect() 方法采用的迂回方式要求捕获 self.pick() 抛出的 LookupError。self.pick() 抛出 LookupError 这一事实也是 接口的一部分,但是在 Python 中没办法声明,只能在文档中说明。

我选择使用 LookupError 异常的原因是,在 Python 的异常层次关系 中,它与 IndexError 和 KeyError 有关,这两个是具体实现 Tombola 所用的数据结构最有可能抛出的异常。据此,实现代码可能会抛出 LookupError、IndexError 或 KeyError 异常。

我们自己定义的 Tombola 抽象基类完成了。为了一睹抽象基类对接口 所做的检查,下面我们尝试使用一个有缺陷的实现来糊弄 Tombola。

不符合 Tombola 要求的子类无法蒙混过关

# 把 Fake 声明为 Tombola 的子类。

class Fake(Tombola):

def pick(self):

return 13

# 创建了 Fake 类,目前没有错误。

Fake

f = Fake()

# 尝试实例化 Fake 时抛出了 TypeError。错误消息十分明确:Python 认为 Fake 是抽象类,因为它没有实现 load 方法,这是 Tombola 抽象基类声明的抽象方法之一。

Traceback (most recent call last):

File "C:/Users/mirror/Desktop/test_something/test3.py", line 38, in

f = Fake()

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

我们的第一个抽象基类定义好了,而且还用它实际验证了一个类。稍后 我们将定义 Tombola 抽象基类的子类,在此之前必须说明抽象基类的 一些编程规则。

11.7.1 抽象基类句法详解

声明抽象基类最简单的方式是继承 abc.ABC 或其他抽象基类。

然而,abc.ABC 是 Python 3.4 新增的类,因此如果你使用的是旧版 Python,那么无法继承现有的抽象基类。此时,必须在 class 语句中使 用 metaclass= 关键字,把值设为 abc.ABCMeta(不是 abc.ABC)。

class Tombola(metaclass=abc.ABCMeta)

# ...

metaclass= 关键字参数是 Python 3 引入的。在 Python 2 中必须使用 __metaclass__ 类属性:

class Tombola(object): # 这是Python 2!!! __metaclass__ = abc.ABCMeta

# ...

元类将在第 21 章讲解。现在,我们暂且把元类理解为一种特殊的类, 同样也把抽象基类理解为一种特殊的类。例如,“常规的”类不会检查子 类,因此这是抽象基类的特殊行为。

除了 @abstractmethod 之外,abc 模块还定义了 @abstractclassmethod、@abstractstaticmethod 和 @abstractproperty 三个装饰器。然而,后三个装饰器从 Python 3.3 起废弃了,因为装饰器可以在 @abstractmethod 上堆叠,那三个就显 得多余了。例如,声明抽象类方法的推荐方式是:

class MyABC(abc.ABC):

@classmethod

@abc.abstractmethod

def an_abstract_classmethod(cls, ...):

pass

在函数上堆叠装饰器的顺序通常很重要,@abstractmethod 的文档就特别指出:与其他方法描述符一起使用时,abstractmethod() 应该放在 最里层,……

也就是说,在 @abstractmethod 和 def 语句之间不能有其他装饰器。

11.7.2 定义Tombola抽象基类的子类

定义好 Tombola 抽象基类之后,我们要开发两个具体子类,满足 Tombola 规定的接口。图中还有将在下一节讨论的虚拟子类。

下例中的 BingoCage 类是在示例 5-8 的基础上修改的,使用了更 好的随机发生器。 BingoCage 实现了所需的抽象方法 load 和 pick,从 Tombola 中继承了 loaded 方法,覆盖了 inspect 方法,还增加了 __call__ 方法。

BingoCage 是 Tombola 的具体子类

import random

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

BingoCage 从 Tombola 中继承了耗时的 loaded 方法和笨拙的 inspect 方法。这两个方法都可以覆盖,变成下例中速度更快的 一行代码。这里想表达的观点是:我们可以偷懒,直接从抽象基类中继 承不是那么理想的具体方法。从 Tombola 中继承的方法没有 BingoCage 自己定义的那么快,不过只要 Tombola 的子类正确实现 pick 和 load 方法,就能提供正确的结果。

下例是 Tombola 接口的另一种实现,虽然与之前不同,但完全有 效。LotteryBlower 打乱“数字球”后没有取出最后一个,而是取出一 个随机位置上的球。

LotteryBlower 是 Tombola 的具体子类, 覆盖了继承的 inspect 和 loaded 方法

class LotteryBlower(Tombola):

def __init__(self, iterable):

# 初始化方法接受任何可迭代对象:把参数构建成列表。避免删除客户自己提供的列表

self._balls = list(iterable)

def load(self, iterable):

self._balls.extend(iterable)

def pick(self):

try:

# 如果范围为空,random.randrange(...) 函数抛出 ValueError, 为了兼容 Tombola,我们捕获它,抛出 LookupError。

position = random.randrange(len(self._balls))

except ValueError:

raise LookupError('pick from empty LotteryBlower')

# 否则,从 self._balls 中取出随机选中的元素。

return self._balls.pop(position)

def loaded(self):

# 覆盖 loaded 方法,避免调用 inspect 方法。我们可以直接处理 self._balls 而不必构建整个有序元组,从而提升速度。

return bool(self._balls)

def inspect(self):

return tuple(sorted(self._balls))

接下来要讲白鹅类型的重要动态特性了:使用 register 方法声明虚拟子类。

Tombola的虚拟子类

白鹅类型的一个基本特性(也是值得用水禽来命名的原因):即便不继 承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我们保证注册的类忠实地实现了抽象基类定义的接口,而 Python 会相信我们, 从而不做检查。如果我们说谎了,那么常规的运行时异常会把我们捕获。

注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之后,注册的类会变成抽象基类的虚拟子类,而且 issubclass 和 isinstance等函数都能识别,但是注册的类不会从抽象基类中继承任何方法或属性。虚拟子类不会继承注册的抽象基类,而且任何时候都不会检 查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了 避免运行时错误,虚拟子类要实现所需的全部方法。

register 方法通常作为普通的函数调用(参见 11.9 节),不过也可以 作为装饰器使用。在下例 中,我们使用装饰器句法实现了 TomboList 类,这是 Tombola 的一个虚拟子类。

TomboList 是 Tombola 的虚拟子类

from random import randrange

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

# 如果是 Python 3.3 或之前的版本,不能把 .register 当作类装饰器 使用,必须使用标准的调用句法。

# Tombola.register(TomboList)

注册之后,可以使用 issubclass 和 isinstance 函数判断 TomboList 是不是 Tombola 的子类:

print(issubclass(TomboList, Tombola))

t = TomboList(range(100))

print(isinstance(t, Tombola))

然而,类的继承关系在一个特殊的类属性中指定—— __mro__,即方法解析顺序(Method Resolution Order)。这个属性的作用很简单,按顺序 列出类及其超类,Python 会按照这个顺序搜索方法。查看 TomboList 类的 __mro__ 属性,你会发现它只列出了“真实的”超类, 即 list 和 object:

>>> TomboList.__mro__

(, , )

Tombolist.__mro__ 中没有 Tombola,因此 Tombolist 没有从 Tombola 中继承任何方法。

我编写了几个类,实现了相同的接口,现在我需要一种编写 doctest 的 方式来涵盖不同的实现。下一节说明如何利用常规类和抽象基类的 API 编写 doctest。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值