13.11 继承

继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管是数据属性还是方法。

举个例子如下。P是一个没有属性的简单类。C从P继承而来(因此是它的子类),也没有属性:

13.11 继承 - 图1

因为P没有属性,C没有继承到什么。下面我们给P添加一些属性:

13.11 继承 - 图2

现在所创建的P有文档字符串(doc)和构造器,当我们实例化P时它被执行,如下面的交互会话所示:

13.11 继承 - 图3

“created an instance”是由init()直接输出的。我们也可显示更多关于父类的信息。我们现在来实例化C,展示init()(构造)方法在执行过程中是如何继承的:

13.11 继承 - 图4

13.11 继承 - 图5

C没有声明init()方法,然而在类C的实例c被创建时,还是会有输出信息。原因在于C继承了P的init()。bases元组列出了其父类P。需要注意的是文档字符串对类,函数/方法,还有模块来说都是唯一的,所以特殊属性doc不会从基类中继承过来。

13.11.1 _bases\类属性

在第13.4.4节中,我们概要地介绍了bases类属性,对任何(子)类,它是一个包含其父类(parent)的集合的元组。注意,我们明确指出“父类”是相对所有基类(它包括了所有祖先类)而言的。那些没有父类的类,它们的bases属性为空。下面我们看一下如何使用bases的。

13.11 继承 - 图6

在上面的例子中,尽管C是A和B的子类(通过B传递继承关系),但C的父类是B,这从它的声明中可以看出,所以,只有B会在C.bases中显示出来。另一方面,D是从两个类A和B中继承而来的(多重继承参见13.11.4节)。

13.11.2 通过继承覆盖方法

我们在P中再写一个函数,然后在其子类中对它进行覆盖。

13.11 继承 - 图7

现在来创建子类C,从父类P派生:

13.11 继承 - 图8

尽管C继承了P的foo()方法,但因为C定义了它自己的foo()方法,所以P中的foo()方法被覆盖(Overrid)。覆盖方法的原因之一是,你的子类可能需要这个方法具有特定或不同的功能。所以,你接下来的问题肯定是:“我还能否调用那个被我覆盖的基类方法呢?”

答案是肯定的,但是这时就需要你去调用一个未绑定的基类方法,明确给出子类的实例,例如下边:

13.11 继承 - 图9

注意,我们上面已经有了一个P的实例p,但上面的这个例子并没有用它。我们不需要P的实例调用P的方法,因为已经有一个P的子类的实例c可用。典型情况下,你不会以这种方式调用父类方法,你会在子类的重写方法里显式地调用基类方法。

13.11 继承 - 图10

注意,在这个(未绑定)方法调用中我们显式地传递了self。一个更好的办法是使用super()内建方法:

13.11 继承 - 图11

super()不但能找到基类方法,而且还为我们传进self,这样我们就不需要做这些事了。现在我们只要调用子类的方法,它会帮你完成一切:

13.11 继承 - 图12

13.11 继承 - 图13核心笔记:重写init不会自动调用基类的init

类似于上面的覆盖非特殊方法,当从一个带构造器init()的类派生,如果你不去覆盖init(),它将会被继承并自动调用。但如果你在子类中覆盖了init(),子类被实例化时,基类的init()就不会被自动调用。这可能会让了解Java的朋友感到吃惊。

13.11 继承 - 图14

13.11 继承 - 图15

如果你还想调用基类的init(),你需要像上边我们刚说的那样,明确指出,使用一个子类的实例去调用基类(未绑定)方法。相应地更新类C,会出现下面预期的执行结果:

13.11 继承 - 图16

上边的例子中,子类的init()方法首先调用了基类的init()方法。这是相当普遍(不是强制)的做法,用来设置初始化基类,然后可以执行子类内部的设置。这个规则之所以有意义的原因是,你希望被继承的类的对象在子类构造器运行前能够很好地被初始化或作好准备工作,因为它(子类)可能需要或设置继承属性。对C++熟悉的朋友,可能会在派生类构造器声明时,通过在声明后面加上冒号和所要调用的所有基类构造器这种形式来调用基类构造器。而在Java中,不管程序员如何处理,子类构造器都会去调用基类的构造器。

Python使用基类名来调用类方法,对应在Java中,是用关键字super来实现的,这就是super()内建函数引入到Python中的原因,这样你就可以“依葫芦画瓢”了:

13.11 继承 - 图17

使用super()的漂亮之处在于,你不需要明确给出任何基类名字……“跑腿儿”的事,它帮你干了!使用super()的重点,使你不需要明确提供父类。这意味着如果你改变了类继承关系,你只需要改一行代码(class语句本身)而不必在大量代码中去查找所有被修改的那个类的名字。

13.11.3 从标准类型派生

经典类中,一个最大的问题是,不能对标准类型进行子类化。幸运的是,在2.2以后的版本中,随着类型(types)和类(class)的统一和新式类的引入,这一点已经被修正。下面,介绍两个子类化Python类型的相关例子,其中一个是可变类型,另一个是不可变类型。

1. 不可变类型的例子

假定你想在金融应用中,应用一个处理浮点型的子类。每次你得到一个贷币值(浮点型给出的),你都需要通过四舍五入,变为带两位小数位的数值。(当然,Decimal类比起标准浮点类型来说是个用来精确保存浮点值的更佳方案,但你还是需要[有时候]对其进行舍入操作!)你的类开始可以这样写:

13.11 继承 - 图18

我们覆盖了new()特殊方法来定制我们的对象,使之和标准Python浮点型(float)有一些区别:我们使用round()内建函数对原浮点型进行舍入操作,然后实例化我们的float, RoundFloat。我们是通过调用父类的构造器来创建真实的对象的,floatnew()。注意,所有的new()方法都是类方法,我们要显式地传入类作为第一个参数,这类似于常见的方法如init()中需要的self。

现在的例子还非常简单,比如,我们知道有一个float,我们仅仅是从一种类型中派生而来等。通常情况下,最好是使用super()内建函数去捕获对应的父类以调用它的new()方法,下面,对它进行这方面的修改:

13.11 继承 - 图19

这个例子还远不够完整,所以,请留意本章我们将使它有更好的表现。下面是一些样例输出:

13.11 继承 - 图20

2. 可变类型的例子

子类化一个可变类型与此类似,你可能不需要使用new()(或甚至init()),因为通常设置不多。一般情况下,你所继承到的类型的默认行为就是你想要的。下例中,我们简单地创建一个新的字典类型,它的keys()方法会自动排序结果:

13.11 继承 - 图21

回忆一下,字典(dictionary)可以由dict()、dict(mapping)、dict(sequence_of_2__tuples)或dict(**kwargs)来创建,看看下面使用新类的例子:

13.11 继承 - 图22

把上面的代码全部加到一个脚本中,然后运行,可以得到下面的输出:

13.11 继承 - 图23

在上例中,通过keys迭代过程是以散列顺序的形式,而使用我们(重写的)keys()方法则将keys变为字母排序方式了。

一定要谨慎,而且要意识到你正在干什么。如果你说“你的方法调用super()过于复杂”,取而代之的是,你更喜欢keys()简简单单(也容易理解),如下所示:

13.11 继承 - 图24

这是本章后面的练习13-19。

13.11.4 多重继承

同C++一样,Python允许子类继承多个基类。这种特性就是通常所说的多重继承。概念容易,但最难的工作是,如何正确找到没有在当前(子)类定义的属性。当使用多重继承时,有两个不同的方面要记住。首先,还是要找到合适的属性。另一个就是当你重写方法时,如何调用对应父类方法以“发挥他们的作用”,同时,在子类中处理好自己的义务。我们将讨论两个方面,但侧重后者,讨论方法解析顺序。

1. 方法解释顺序(MRO)

在Python 2.2以前的版本中,算法非常简单:深度优先,从左至右进行搜索,取得在子类中使用的属性。其他Python算法只是覆盖被找到的名字,多重继承则取找到的第一个名字。

由于类,类型和内建类型的子类,都经过全新改造,有了新的结构,这种算法不再可行。这样一种新的MRO (Method Resolution Order)算法被开发出来,在2.2版本中初次登场,是一个好的尝试,但有一个缺陷(看下面的核心笔记)。这在2.3版本中立即被修改,也就是今天还在使用的版本。

精确顺序解释很复杂,超出了本文的范畴,但你可以去阅读本节后面的参考书目提到的有关内容。这里提一下,新的查询方法是采用广度优先,而不是深度优先。

13.11 继承 - 图25核心笔记:Python 2.2使用一种唯一但不完善的MRO

Python 2.2是首个使用新式MRO的版本,它必须取代经典类中的算法,原因在上面已谈到过。在2.2版本中,算法基本思想是根据每个祖先类的继承结构,编译出一张列表,包括搜索到的类,按策略删除重复的。然而,在Python核心开发人员邮件列表中,有人指出,在维护单调性方面失败过(顺序保存),必须使用新的C3算法替换,也就是从2.3版开始使用的新算法。

下面的示例,展示经典类和新式类中,方法解释顺序有什么不同。

2. 简单属性查找示例

下面这个例子将对两种类的方案不同处做一展示。脚本由一组父类,一组子类,还有一个子孙类组成。

13.11 继承 - 图26

13.11 继承 - 图27

在图13-2中,我们看到父类、子类及子孙类的关系。P1中定义了foo(), P2定义了foo()和bar(),C2定义了bar()。下面举例说明一下经典类和新式类的行为。

13.11 继承 - 图28

图 13-2 父类,子类及子孙类的关系图,还有它们各自定义的方法

(1)经典类

首先来使用经典类。通过在交互式解释器中执行上面的声明,我们可以验证经典类使用的解释顺序,深度优先,从左至右:

13.11 继承 - 图29

当调用foo()时,它首先在当前类(GC)中查找。如果没找到,就向上查找最亲的父类,C1。查找未遂,就继续沿树上访到父类P1,foo()被找到。

同样,对bar()来说,它通过搜索GC, C1, P1然后在P2中找到。因为使用这种解释顺序的缘故,C2.bar()根本就不会被搜索了。

现在,你可能在想,“我更愿意调用C2的bar()方法,因为它在继承树上和我更亲近些,这样才会更合适”。在这种情况下,你当然还可以使用它,但你必须调用它的合法的全名,采用典型的非绑定方式去调用,并且提供一个合法的实例:

13.11 继承 - 图30

(2)新式类

取消类P1和类P2声明中的对(object)的注释,重新执行一下。新式方法的查询有一些不同:

13.11 继承 - 图31

与沿着继承树一步一步上溯不同,它首先查找同胞兄弟,采用一种广度优先的方式。当查找foo(),它检查GC,然后是C1和C2,然后在P1中找到。如果P1中没有,查找将会到达P2。foo()的底线是,包括经典类和新式类都会在P1中找到它,然而它们虽然是同归,但殊途!

然而,bar()的结果是不同的。它搜索GC和C1,紧接着在C2中找到了。这样,就不会再继续搜索到祖父P1和P2。这种情况下,新的解释方式更适合那种要求查找GC更亲近的bar()的方案。当然,如果你还需要调用上一级,只要按前述方法,使用非绑定的方式去做,即可。

13.11 继承 - 图32

新式类也有一个mro属性,告诉你查找顺序是怎样的:

13.11 继承 - 图33

3. *菱形效应引起MRO问题

经典类方法解释不会带来很多问题。它很容易解释,并理解。大部分类都是单继承的,多重继承只限用在对两个完全不相关的类进行联合。这就是术语mixin类(或者“mix-ins”)的由来。

为什么经典类MRO会失败

在版本2.2中,类型与类的统一,带来了一个新的“问题”,波及所有从object(所有类型的祖先类)派生出来的(根)类,一个简单的继承结构变成了一个菱形。从Guido van Rossum的文章中得到下面的灵感,打个比方,你有经典类B和C, C覆盖了构造器,B没有,D从B和C继承而来:

13.11 继承 - 图34

当我们实例化D,得到:

13.11 继承 - 图35

图13-3为B, C和D的类继承结构,现在把代码改为采用新式类的方式,问题也就产生了:

13.11 继承 - 图36

图 13-3 继承的问题是由于在新式类中,需要出现基类,这样就在继承结构中,形成了一个菱形。D的实例上溯时,不应当错过C,但不能两次上溯到A(因为B和C都从A派生)。去读读贵铎·范·罗萨姆的文章中有关“协作方法”的部分,可以得到更深地理解。

13.11 继承 - 图37

代码中仅仅是在两个类声明中加入了(object),对吗?没错,但从图中,你可以看出,继承结构已变成了一个菱形;真正的问题就存在于MRO了。如果我们使用经典类的MRO,当实例化D时,不再得到C.init()之结果…..而是得到object.init()!这就是为什么MRO需要修改的真正原因。

尽管我们看到了,在上面的例子中,类GC的属性查找路径被改变了,但你不需要担心会有大量的代码崩溃。经典类将沿用老式MRO,而新式类将使用它自己的MRO。还有,如果你不需要用到新式类中的所有特性,可以继续使用经典类进行开发,不会有问题的。

4. 总结

经典类,使用深度优先算法。因为新式类继承自object,新的菱形类继承结构出现,问题也就接着而来了,所以必须新建一个MRO。

你可以在下面的链接中读在更多有关新式类、MRO的文章。

Guido van Rossum有关类型和类统一的文章:

http://www.python.org/download/releases/2.2.3/descrintro

PEP 252:使类型看起来更像类

http://www.python.org/doc/peps/pep-0252

“Python 2.2新亮点”文档

http://www.python.org/doc/2.2.3/whatsnew

论文:Python 2.3方法解释顺序

http://python.org/download/releases/2.3/mro/