python 定义并使用抽象基类

本文介绍了一个抽象基类Tombola的设计,该基类支持随机无重复抽取元素的功能,详细解释了抽象方法、具体方法及虚拟子类的实现,并展示了如何通过Tombola接口检查子类的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、定义一个抽象基类
我们现在定义一个抽象基类 ,它的职责之一是,支持用户提供随 机挑选的无重复类。

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

Tombola 抽象基类有四个方法,其中两个是抽象方法。
抽象方法
1, .load(…):把元素放入容器。
2,.pick():从容器中随机拿出一个元素,返回选中的元素。
具体方法
1, .loaded():如果容器中至少有一个元素,返回 True。
2,.inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内 部的顺序不保留)。
在这里插入图片描述

import abc


# 自己定义的抽象基类要继承 abc.ABC。
class Tombola(abc.ABC):

    # 抽象方法使用 @abstractmethod 装饰器标记,而且定义体中通常只有文档字符串。
    @abc.abstractmethod
    def load(self, iterable):
        """
        从可迭代对象中添加元素。
        """

    # 根据文档字符串,如果没有元素可选,应该抛出 LookupError。
    @abc.abstractmethod
    def pick(self):
        """
        随机删除元素,然后将其返回。
        如果实例为空,这个方法应该抛出`LookupError`。
        """

    # 抽象基类可以包含具体方法
    def loaded(self):
        """
        如果至少有一个元素,返回`True`,否则返回`False`。
        """
        return bool(self.inspect())  # 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具 体方法、抽象方法或特性)。

    def inspect(self):
        """返回一个有序元组,由当前元素构成。"""
        items = []
        while True:  # 我们不知道具体子类如何存储元素,不过为了得到 inspect 的结果,我们可以不断调 用 .pick() 方法,把 Tombola 清空……
            try:
                items.append(self.pick())
            except LookupError:
                break
            self.load(items)  # 然后再使用 .load(...) 把所有元素放回去
            return tuple(sorted(items))

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

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

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

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

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

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

import abc


# 自己定义的抽象基类要继承 abc.ABC。
class Tombola(abc.ABC):

    # 抽象方法使用 @abstractmethod 装饰器标记,而且定义体中通常只有文档字符串。
    @abc.abstractmethod
    def load(self, iterable):
        """
        从可迭代对象中添加元素。
        """

    # 根据文档字符串,如果没有元素可选,应该抛出 LookupError。
    @abc.abstractmethod
    def pick(self):
        """
        随机删除元素,然后将其返回。
        如果实例为空,这个方法应该抛出`LookupError`。
        """

    # 抽象基类可以包含具体方法
    def loaded(self):
        """
        如果至少有一个元素,返回`True`,否则返回`False`。
        """
        return bool(self.inspect())  # 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具 体方法、抽象方法或特性)。

    def inspect(self):
        """返回一个有序元组,由当前元素构成。"""
        items = []
        while True:  # 我们不知道具体子类如何存储元素,不过为了得到 inspect 的结果,我们可以不断调 用 .pick() 方法,把 Tombola 清空……
            try:
                items.append(self.pick())
            except LookupError:
                break
            self.load(items)  # 然后再使用 .load(...) 把所有元素放回去
            return tuple(sorted(items))


# 把 Fake 声明为 Tombola 的子类。
class Fake(Tombola):
    def pick(self):
        return 13


if __name__ == "__main__":
    # 创建了 Fake 类,目前没有错误。
    print(Fake)
    # 尝试实例化 Fake 时抛出了 TypeError。错误消息十分明确:Python 认为 Fake 是抽象类, 因为它没有实现 load 方法,这是 Tombola 抽象基类声明的抽象方法之一。
    f = Fake()


在这里插入图片描述
在之前的博客中也说明这个问题,继承抽象基类要实现抽象类的抽象方法,不然在实例化的时候就会出错。

二、抽象基类句法详解
1,声明抽象基类最简单的方式是继承 abc.ABC 或其他抽象基类。
2,然而,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):   # python2
	__metaclass__ = abc.ABCMeta

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

class MyABC(abc.ABC): 
	@classmethod 
	@abc.abstractmethod 
	def an_abstract_classmethod(cls, ...): 
		pass

注意: 在函数上堆叠装饰器的顺序通常很重要,@abstractmethod 的文档就特别指出:与其他方法描述符一起使用时,abstractmethod() 应该放在最里层,也就是说,在 @abstractmethoddef 语句之间不能有其他装饰器。

三、定义Tombola抽象基类的子类
定义好 Tombola 抽象基类之后,我们要开发两个具体子类,满足 Tombola 规定的接口。
BingoCage 类,使用了更好的随机发生器。 BingoCage 实现了所需的抽象方法 loadpick,从 Tombola 中继承了 loaded 方法,覆盖了 inspect 方法,还增加了 __call__ 方法。

import abc
import random


# 自己定义的抽象基类要继承 abc.ABC。
class Tombola(abc.ABC):

    # 抽象方法使用 @abstractmethod 装饰器标记,而且定义体中通常只有文档字符串。
    @abc.abstractmethod
    def load(self, iterable):
        """
        从可迭代对象中添加元素。
        """

    # 根据文档字符串,如果没有元素可选,应该抛出 LookupError。
    @abc.abstractmethod
    def pick(self):
        """
        随机删除元素,然后将其返回。
        如果实例为空,这个方法应该抛出`LookupError`。
        """

    # 抽象基类可以包含具体方法
    def loaded(self):
        """
        如果至少有一个元素,返回`True`,否则返回`False`。
        """
        return bool(self.inspect())  # 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具 体方法、抽象方法或特性)。

    def inspect(self):
        """返回一个有序元组,由当前元素构成。"""
        items = []
        while True:  # 我们不知道具体子类如何存储元素,不过为了得到 inspect 的结果,我们可以不断调 用 .pick() 方法,把 Tombola 清空……
            try:
                items.append(self.pick())
            except LookupError:
                break
            self.load(items)  # 然后再使用 .load(...) 把所有元素放回去
            return tuple(sorted(items))


# 把 Fake 声明为 Tombola 的子类。
class Fake(Tombola):
    def pick(self):
        return 13


# 明确指定 BingoCage 类扩展 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)
        # 没有使用 random.shuffle() 函数,而是使用 SystemRandom 实例的 .shuffle() 方法。
        self._randomizer.shuffle(self._items)

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

    # __call__ 它没必要满足 Tombola 接口,添加额外的方法没有问题。
    def __call__(self):
        self.pick()


if __name__ == "__main__":
    # 创建了 Fake 类,目前没有错误。
    # print(Fake)
    # 尝试实例化 Fake 时抛出了 TypeError。错误消息十分明确:Python 认为 Fake 是抽象类, 因为它没有实现 load 方法,这是 Tombola 抽象基类声明的抽象方法之一。
    # f = Fake()
    pass

当然这里也能覆盖抽象类的原有非抽象方法。

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

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

虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽 象基类的接口,即便在实例化时也不会检查。为了避免运行时错误,虚拟子 类要实现所需的全部方法。

register 方法通常作为普通的函数调用,不过也可以作为装饰器使用。示例如下:

from random import randrange

from python_page.Tombola import Tombola


# 把 Tombolist 注册为 Tombola 的虚拟子类。
@Tombola.register
class TomboList(list):  # 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  # Tombolist.load 与 list.extend 一样。

    def loaded(self):  # loaded 方法委托 bool 函数
        return bool(self)

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

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


if  __name__ == "__main__":
    # 注册之后,可以使用 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:
    print(TomboList.__mro__)


运行结果:
在这里插入图片描述
Tombolist.__mro__ 中没有 Tombola,因此 Tombolist 没有从 Tombola 中继承任何方法。

五、Tombola子类的测试方法
编写的 Tombola 示例测试脚本用到两个类属性,用它们内省类的继承关系。

__subclasses__()   # 这个方法返回类的直接子类列表,不含虚拟子类。

_abc_registry  # 只有抽象基类有这个数据属性,其值是一个 WeakSet 对象,即抽象类注册的虚拟子类的 弱引用

六、Python使用register的方式
Tombola.register 当作类装饰器使用。在 Python 3.3 之前的版本中不能这样使用 register,必须在定义类之后像普通函数那样调用。
虽然现在可以把 register 当作装饰器使用了,但更常见的做法还是把它当作函数使用,用于注册其他地方定义的类。例如,在 collections.abc 模块的源码中(https://hg.python.org/cpython/file/3.4/Lib/_collections_abc.py),是这样把内置类型 tuple、strrangememoryview 注册为 Sequence虚拟子类的:

Sequence.register(tuple) 
Sequence.register(str) 
Sequence.register(range) 
Sequence.register(memoryview)

其他几个内置类型在 _collections_abc.py 文件(https://hg.python.org/cpython/file/3.4/Lib/_collections_abc.py)中注册为抽象基类的虚拟子类。这些类型在导入模块时注册,这样做是可以的,因为必须导入才能使用抽象基类:能访问 MutableMapping 才能编写isinstance(my_dict, MutableMapping)

七、鹅的行为有可能像鸭子
在这里插入图片描述
经 issubclass 函数确认(isinstance 函数也会得出相同的结论),Aabc.Sized的子类,这是因为 abc.Sized 实现了一个特殊的类方法,名为 __subclasshook__
下面看一下源码:

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
			

由此可见:__subclasshook__ 在白鹅类型中添加了一些鸭子类型的踪迹。我们可以使用抽象基类定义正式接口,可以始终使用 isinstance 检查,也可以完全使用不相关的类,只要实现特定的方法即可(或者做些事情让__subclasshook__ 信服)。当然,只有提供 __subclasshook__方法的抽象基类才能这么做。不过我们自己在定义抽象类的时候最好不要实现__subclasshook__方法,这样做感觉不是很合理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值