pyhton中定义并使用一个抽象基类

定义并使用一个抽象基类

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

将使用现实世界中的物品命名这个抽象基类:宾果机和彩票机是随机从有限的集合中挑选物品的机器,选出的物品没有重复,直到选完为止。把这个抽象基类命名为Tombola,这是宾果机和打乱数字的滚动容器的意大利名。

Tombola抽象基类有四个方法,其中两个是抽象方法。
• .load(…):把元素放入容器。
• .pick():从容器中随机拿出一个元素,返回选中的元素。

另外两个是具体方法。

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

Tombola抽象基类和三个具体实现

在这里插入图片描述
一个抽象基类和三个子类的UML类图。根据UML的约定,Tombola抽象基类和它的抽象方法使用斜体。虚线箭头用于表示接口实现,这里它表示TomboList是Tombola的虚拟子类,因为TomboList是注册的。
Tombola抽象基类的定义如示例1:

#示例1:Tombola是抽象基类,有两个抽象方法和两个具体方法
import abc

class Tombola(abc.ABC):  #➊ 
    @abc.abstractmethod
    def load(self,iterable):  #➋ 
        """从可迭代对象中添加元素"""
        
    @abc.abstractmethod
    def pick(self):  3"""随机删除元素,然后将其返回。
        如果示例为空,这个方法应该抛出‘LookupError`。
        """
        
    def loaded(self):  #➍ 
        """如果至少有一个元素,返回`True`,否则返回`False`。"""
        return bool(self.inspect()) #➎ 
    
    def inspect(self):
        istems = []
        while True:   #➏ 
            try:
                times.append(self.pick())
            except LookError:
                break
        self.load(items) #➐ 
        return tuple(sorted(items)) 

➊ 自己定义的抽象基类要继承abc.ABC。
➋ 抽象方法使用@abstractmethod装饰器标记,而且定义体中通常只有文档字符串。
➌ 根据文档字符串,如果没有元素可选,应该抛出LookupError。
➍ 抽象基类可以包含具体方法。
➎ 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具体方法、抽象方法或特性)。
➏ 不知道具体子类如何存储元素,不过为了得到inspect的结果,可以不断调用.pick()方法,把Tombola清空……
➐ ……然后再使用.load(…)把所有元素放回去。

#示例2:异常类的部分层次结构
BaseException 
 ├── SystemExit 
 ├── KeyboardInterrupt 
 ├── GeneratorExit 
 └── Exception 
      ├── StopIteration 
      ├── ArithmeticError 
      │    ├── FloatingPointError 
      │    ├── OverflowError 
      │    └── ZeroDivisionError 
      ├── AssertionError 
      ├── AttributeError 
      ├── BufferError 
      ├── EOFError 
      ├── ImportError 
      ├── LookupError  #➊ 
      │    ├── IndexError  #➋ 
      │    └── KeyError  #➌ 
      ├── MemoryError 
      ... etc.

➊ 我们在Tombola.inspect方法中处理的是LookupError异常。
➋ IndexError是LookupError的子类,尝试从序列中获取索引超过最后位置的元素时抛出。
➌ 使用不存在的键从映射中获取元素时,抛出KeyError异常。

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

#示例3:不符合Tombola要求的子类无法蒙混过关
>>> from tombola import Tombola 
>>> class Fake(Tombola):  # ➊ 
...     def pick(self): 
...         return 13 
... 
>>> Fake  # ➋ 
<class '__main__.Fake'> 
>>> f = Fake()  # ➌ 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
TypeError: Can't instantiate abstract class Fake with abstract methods load

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

抽象基类句法详解

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

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

class Tombola(metaclass=abc.ABCMeta):
# …

metaclass=关键字参数是Python 3引入的。在Python 2中必须使用__metaclass__类属性:
class Tombola(object): # 这是Python 2!!!
metaclass = abc.ABCMeta
# …

定义Tombola抽象基类的子类

定义好Tombola抽象基类之后,要开发两个具体子类,满足Tombola规定的接口。

#示例4:ingoCage是Tombola的具体子类
import random 
 
from tombola import Tombola 
 
 
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类。
➋ 假设我们将在线上游戏中使用这个。random.SystemRandom使用os.urandom(…)函数实现random API。
➌ 委托.load(…)方法实现初始加载。
➍ 没有使用random.shuffle()函数,而是使用SystemRandom实例的.shuffle()方法。
➎ 如果self._items为空,抛出异常,并设定错误消息。
➏ __call__没必要满足Tombola接口,添加额外的方法没有问题。

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

#示例5:LotteryBlower是Tombola的具体子类,覆盖了继承的inspect和loaded方法
import random 
 
from tombola import Tombola 
 
 
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 LotteryBlower') 
        return self._balls.pop(position)  #➌ 
 
    def loaded(self):  #➍ 
        return bool(self._balls) 
 
    def inspect(self):  #➎ 
        return tuple(sorted(self._balls))

➊ 初始化方法接受任何可迭代对象:把参数构建成列表。
➋ 如果范围为空,random.randrange(…)函数抛出ValueError,为了兼容Tombola,我们捕获它,抛出LookupError。
➌ 否则,从self._balls中取出随机选中的元素。
➍ 覆盖loaded方法,避免调用inspect方法(示例1中的Tombola.loaded方法是这么做的)。我们可以直接处理self._balls而不必构建整个有序元组,从而提升速度。
➎ 使用一行代码覆盖inspect方法。

Tombola的虚拟子类

白鹅类型
白鹅类型对接口有明确定义,比如不可变序列(Sequence),需要实现__contains__,iterlengetitemreversed,index,count
对于其中的抽象方法,子类在继承时必须具体化,其余非抽象方法在继承时可以自动获得,Sequence序列必须具体化的抽象方法是__len__和__getitem__。

白鹅类型的一个基本特性(也是值得用水禽来命名的原因):即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。

注册虚拟子类的方式是在抽象基类上调用register方法。

Tombola的一个虚拟子类(list的真实子类和Tombola的虚拟子类)

在这里插入图片描述

#示例6:TomboList是Tombola的虚拟子类
from random import randrange 
 
from tombola import Tombola 
 
@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(TomboList)  # ➐

➊ 把Tombolist注册为Tombola的虚拟子类。
➋ Tombolist扩展list。
➌ Tombolist从list中继承__bool__方法,列表不为空时返回True。
➍ pick调用继承自list的self.pop方法,传入一个随机的元素索引。
➎ Tombolist.load与list.extend一样。
➏ loaded方法委托bool函数。
➐ 如果是Python 3.3或之前的版本,不能把.register当作类装饰器使用,必须使用标准的调用句法。

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

>>> from tombola import Tombola 
>>> from tombolist import TomboList 
>>> issubclass(TomboList, Tombola) 
True 
>>> t = TomboList(range(100)) 
>>> isinstance(t, Tombola) 
True

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

>>> TomboList.__mro__ 
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值