>>> from env_helper import info; info()
页面更新时间: 2023-04-18 20:57:22
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-7-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

9.3. 超类、接口与基类

子类扩展了超类的定义。要指定超类,可在class语句中的类名后加上超类名,并将其用圆括号括起。

>>> class Filter:
>>>     def init(self):
>>>         self.blocked = []
>>>     def filter(self, sequence):
>>>         return [x for x in sequence if x not in self.blocked]
>>>
>>> f = Filter()
>>> f.init()
>>> f.filter([1, 2, 3])
[1, 2, 3]

Filter是一个过滤序列的通用类。实际上它不会过滤掉任何东西。Filter类的用途在于可用作其他类的基类(超类)。

>>> class SPAMFilter(Filter): # SPAMFilter是Filter的子类
>>>     def init(self): # 重写超类Filter的方法init
>>>         self.blocked = ['SPAM']
>>> s = SPAMFilter()
>>> s.init()
>>> s.filter(['SPAM', 'SPAM', 'SPAM', 'SPAM', 'eggs', 'bacon', 'SPAM'])
['eggs', 'bacon']

请注意SPAMFilter类的定义中有两个要点。

  1. 以提供新定义的方式重写了Filter类中方法init的定义。

  2. 直接从Filter类继承了方法filter的定义,因此无需重新编写其定义。

9.3.1. 多个超类

为说明如何继承多个类,下面来创建几个类。

>>> class Calculator:
>>>     def calculate(self, expression):
>>>         self.value = eval(expression)
>>>
>>> class Talker:
>>>     def talk(self):
>>>         print('Hi, my value is', self.value)
>>>
>>> class TalkingCalculator(Calculator, Talker):
>>>     pass

子类 TalkingCalculator 本身未实现任务功能,其所有的行为都是从超类那里继承的: 从 Calculator 那里继承 calculate() ,并从 Talker 那里继承 talk() ,它成了会说话的计算器。

>>> tc = TalkingCalculator()
>>> tc.calculate('1 + 2 * 3')
>>> tc.talk()
Hi, my value is 7

这被称为多重继承,从开发语言方面来讲是一个强大的功能。 然而,除非万不得已,否则应避免使用多重继承,因为在有些情况下,它可能带来意外的“并发症”。

使用多重继承时,有一点务必注意:如果多个超类以不同的方式实现了同一个方法 (即有多个同名方法),必须在 class 语句中小心排列这些超类,因为位于前面的类的方法将屏蔽位于后面的类的方法。 因此,在前面的示例中,如果 Calculator() 类包含方法 talk ,那么这个方法将覆盖 Talker 类的方法 talk()(导致它不可访问)。

可以像下面这样反转超类的排列顺序:

>>> class TalkingCalculator(Talker, Calculator): pass

将导致 Talker 的方法 talk 是可以访问的。 多个超类的超类相同时,查找特定方法或属性时访问超类的顺序称为方法解析顺序(MRO),它使用的算法非常复杂。 所幸其效果很好,你可能根本无需担心。

9.3.2. 接口

接口这一概念与多态相关。处理多态对象时,你只关心其接口(协议)——对外暴露的方 法和属性。在Python中,不显式地指定对象必须包含哪些方法才能用作参数。

通常,你要求对象遵循特定的接口(即实现特定的方法),但如果需要,也可非常灵活地提 出要求:不是直接调用方法并期待一切顺利,而是检查所需的方法是否存在;如果不存在,就改 弦易辙。

>>> hasattr(tc, 'talk')
True
>>> hasattr(tc, 'fnord')
False

在上述代码中,你发现tc包含属性talk(指向一个方法),但没有属性fnord。如果你愿意,还可以检查属性talk是否是可调用的。

>>> callable(getattr(tc, 'talk', None))
True
>>> callable(getattr(tc, 'fnord', None))
False

请注意,这里没有在if语句中使用hasattr并直接访问属性,而是使用了getattr(,然后对返回的对象调用callable。

注意 setattr与getattr功能相反,可用于设置对象的属性:

>>> setattr(tc, 'name', 'Mr. Gumby')
>>> tc.name
'Mr. Gumby'

要查看对象中存储的所有值,可检查其 dict 属性。如果要确定对象是由什么组成的,应 研究模块inspect。这个模块主要供高级用户创建对象浏览器以及其他需要这种功能的类似程序。。

9.3.3. 抽象基类

然而,有比手工检查各个方法更好的选择。在历史上的大部分时间内,Python几乎都只依赖 于鸭子类型,即假设所有对象都能完成其工作,同时偶尔使用hasattr来检查所需的方法是否存 在。很多其他语言(如Java和Go)都采用显式指定接口的理念,而有些第三方模块提供了这种理 念的各种实现。最终,Python通过引入模块abc提供了官方解决方案。这个模块为所谓的抽象基 类提供了支持。一般而言,抽象类是不能(至少是不应该)实例化的类,其职责是定义子类应实 现的一组抽象方法。下面是一个简单的示例:

>>> from abc import ABC, abstractmethod
>>> class Talker(ABC):
>>>     @abstractmethod
>>>     def talk(self):
>>>         pass

形如 @this 的东西被称为装饰器,这里的要点是你使用@abstractmethod来将方法标记为抽象的——在子类中必须实现的方法。

注意 如果你使用的是较旧的Python版本,将无法在模块abc中找到ABC类。在这种情况下,需要 导入ABCMeta,并在类定义开头包含代码行 metaclass = ABCMeta(紧跟在class语句后 面并缩进)。如果你使用的是3.4之前的Python 3版本,也可使用Talker(metaclass=ABCMeta) 代替Talker(ABC)。

抽象类(即包含抽象方法的类)最重要的特征是不能实例化。

>>> Talker()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Talker with abstract methods talk

假设像下面这样从它派生出一个子类:

>>> class Knigget(Talker):
>>>     pass

由于没有重写方法talk,因此这个类也是抽象的,不能实例化。如果你试图这样做,将出现 类似于前面的错误消息。然而,你可重新编写这个类,使其实现要求的方法。

>>> class Knigget(Talker):
>>>     def talk(self):
>>>         print("Ni!")

现在实例化它没有任何问题。这是抽象基类的主要用途,而且只有在这种情形下使用 isinstance才是妥当的:如果先检查给定的实例确实是Talker对象,就能相信这个实例在需要的 情况下有方法talk。

>>> k = Knigget()
>>> isinstance(k, Talker)
True
>>> k.talk()
Ni!

然而,还缺少一个重要的部分——让isinstance的多态程度更高的部分。正如你看到的,抽 象基类让我们能够本着鸭子类型的精神使用这种实例检查!我们不关心对象是什么,只关心对象 能做什么(它实现了哪些方法)。因此,只要实现了方法talk,即便不是Talker的子类,依然能 够通过类型检查。下面来创建另一个类。

>>> class Herring:
>>>     def talk(self):
>>>         print("Blub.")

这个类的实例能够通过是否为Talker对象的检查,可它并不是Talker对象。

>>> h = Herring()
>>> isinstance(h, Talker)
False

诚然,你可从Talker派生出Herring,这样就万事大吉了,但Herring可能是从他人的模块中 导入的。在这种情况下,就无法采取这样的做法。为解决这个问题,你可将Herring注册为Talker(而不从Herring和Talker派生出子类),这样所有的Herring对象都将被视为Talker对象。

>>> Talker.register(Herring)
__main__.Herring

<class ’ main .Herring’>

>>> isinstance(h, Talker)
True
>>> issubclass(Herring, Talker)
True

然而,这种做法存在一个缺点,就是直接从抽象类派生提供的保障没有了。

>>> class Clam:
...
pass
...
>>> Talker.register(Clam)
<class '__main__.Clam'>
>>> issubclass(Clam, Talker)
True
>>> c = Clam()
>>> isinstance(c, Talker)
True
>>> c.talk()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Clam' object has no attribute 'talk'