• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Python面向对象:继承与多态

Python编程 彭东稳 7年前 (2017-12-11) 21710次浏览 已收录 0个评论

一、继承与多态

在面向对象(OOP)程序设计中,当我们定义一个类的时候,可以从某个现有的类继承,新的类称为子类(Subclass),而被继承的类称为基类、父类或超类(Base class、Super class)。类中继承就是子类获得父类的一些方法和属性(类属性、实例属性、类方法、实例方法、静态方法),这里使用一些也就是说有些是子类继承不到的,比如私有属性。

简单来说继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。另外在 Python 2.4 开始,Python 引入了 object 这个最上层的根类,但需要显式指定,也被称之为经典类;而在 Python 3 中所有类默认都继承 object 根类,无需显式指定,也被称之为新式类。

比如,编写一个 Base 类,定义一个 init 方法:

当我们需要编写 Sub1 和 Sub2 类时,就可以直接从 Base 类继承:

对于 Sub1 和 Sub2 来说,Base 就是它的父类;对于 Base 来说,Sub1 和 Sub2 就是它的子类。

继承有什么好处?最大的好处是子类获得了父类的一些属性和方法。由于 Base 实现有 init 方法,因此,Sub1 和 Sub2 作为它的子类,什么事也没干,就自动拥有了 init:

当然,子类也可以新增自己特有的方法,也可以把父类不适合的方法覆盖重写。

那么什么样的属性是子类无法继承的呢?就是以__开头的私有变量是子类无法继承的,如下:

结果如下:

我们知道私有变量内部是通过改变变量名来实现的,从报错结果来看就是因为变量名问题,当然通过特殊方法也是可以继承到的,但是通常不这么干。

下面再来尝试一个带参数的类:

实例化子类:

报错了,告诉我们需要传参数,也就说明,当我们初始化子类时会先初始化父类。当父类定义了带参数的初始化方法时,子类要显式的定义初始化方法,并且在初始化方法里初始化父类,如下:

注意在 Python 3 里语法有所改变:你可以用super().__init__()替换super(Sub, self).__init__(),更简洁,更 Nice。

使用 super 用来返回 super 对象(super 是 Python 3 内置用在类继承中的方法),可以使用 super 对象调用父类的方法及属性。再次实例化子类,如下:

通过使用 super,就可以正常实例化子类了;当然在实例化子类时,你也可以直接给子类传参,如下:

但是通常不这么干,为了代码易读易用。

另外,super 还有一个作用就是当父类和子类有相同的方法名称时,其实父类不会生效,此时可以通过 super 来初始化父类。测试代码如下:

然后,我们在实例化子类时就会报找不到 self.x 属性,如下:

然后我们使用 super 在子类中初始化一下父类就可以正常识别父类属性了。

但是一般也不要这么写,在子类如果不使用跟父类相同名称的方法名就没这个问题。关于 super 更多细节可以看相关文档。

继承的第二个好处需要我们对代码做一点改进;通过上面的例子,我们看到了,既然是继承,我们得到了想要的结果。但是当子类和父类都存在相同的方法或属性时,我们说,子类的方法或属性会覆盖了父类的方法或属性,在代码运行的时候,总是会调用子类的方法或属性。这样,我们就获得了继承的另一个好处:多态。

多态顾名思义是指“多种形态”,具体是指父类定义的方法可以调用子类实现的方法。不同的子类有不同的实现,从而给父类的方法带来了多样的不同行为。换种方式描述,主程序只调用接口或缺省类,而使用者提供实现类,两者结合起来,完成业务功能。

父类 Base 定义的 init 方法调用了子类实现的 example 方法,子类的方法可以对父类定义的方法进行覆盖,父类的 example 方法被隐藏起来了。

要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个 class 的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和 Python 自带的数据类型,比如 str、list、dict 没什么两样:

判断一个变量是否是某个类型可以用 isinstance() 判断,如下:

看上去 A、B、C 对应 Base、Sub、str 三种类型都没有问题,接下来测试一下 B 是否是 Base 类型:

也为真,这说明不仅 B 是 Sub 类型,同时也是 Base 类型。

不过仔细想想,这是有道理的,因为 Sub 是从 Base 继承下来的,当我们创建了一个 Sub 的实例 B 时,我们认为 B 的数据类型是 Sub 没错,但 B 同时也是 Base 也没错,Sub 本来就是 Base 的一种!

所以,在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行:

Sub 可以看成 Base,但 Base 不可以看成 Sub。

要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个 Base 类型的变量:

当我们传入 Base 的实例时,run() 就打印出:

当我们传入 Sub1 的实例时,run() 就打印出:

看上去没啥意思,但是仔细想想,现在,如果我们不管定义多少个从 Base 派生的子类,不必对 run() 做任何修改。实际上,任何依赖 Base 作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。

多态的好处就是,当我们需要传入 Sub、Sub1、Sub2……时,我们只需要接收 Base 类型就可以了,因为 Sub、Sub1、Sub2…… 都是 Base 类型,然后,按照Base类型进行操作即可。由于 Base 类型有 init 方法,因此,传入的任意类型,只要是Base类或者子类,就会自动调用实际类型的 init 方法,这就是多态的意思。

对于一个变量,我们只需要知道它是 Base 类型,无需确切地知道它的子类型,就可以放心地调用 init 方法,而具体调用的方法或属性是作用在 Base、Sub 还是 Sub1 对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种 Base 的子类时,只要确保方法或属性编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

对扩展开放:允许新增 Base 子类;

对修改封闭:不需要修改依赖 Base 类型的 run() 等函数。

继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系。而任何类,最终都可以追溯到根类 object,这些继承关系看上去就像一颗倒着的树。

二、多重继承

Python 是支持类中多重继承的,概念虽然容易,但是困难的工作是如果子类调用一个自身没有定义的属性,它是按照何种顺序去到父类寻找呢,尤其是众多父类中有多个都包含该同名属性。

Python 的类分为经典类与新式类。Python 2.7 之前的版本中可以采用经典类,经典类继承父类的顺序采用深度优先算法,但在 Python 3 之后的版本就只承认新式类了。新式类在 Python 2.2 之后的版本中都可以使用,新式类的继承顺序采用 C3 算法,其继承顺序可以通过查看 MRO 列表获取。经典类没有__MRO__instance.mro()调用,而新式类有。

经典类中采用深度优先的匹配方法,可能导致在查询继承树中绕过后面的父类(在 Python 2 中测试,Python 默认使用新式类):

结果如下:

新式类采用 C3 算法(区别于广度优先的原则)进行搜索,若使用新式类:

结果如下:

这里为什么类 D 没有打印出来呢?因为类C跟类D有相同的方法,子类把父类给覆盖了。如果也想打印父类可以使用 super 方法(下面会介绍 super),如下:

此时再去初始化类A,结果如下:

经典类和新式类各自搜索的顺序如下图所示:

Python面向对象:继承与多态

C3算法最早被提出是用于Lisp的,应用在Python中是为了解决原来基于深度优先搜索算法不满足本地优先级,和单调性的问题。

  • 本地优先级:指声明时父类的顺序,比如A(B,C),如果访问A类对象属性时,应该根据声明顺序,优先查找B类,然后再查找C类。
  • 单调性:如果在A的解析顺序中,B排在C的前面,那么在A的所有子类里,也必须满足这个顺序。

对于下面这一段程序:

当初始化实例F = F()时,使用深度优先搜索,广度优先搜索及C3算法的不同搜索顺序如下:

Python面向对象:继承与多态

对于新式类,可以用instance.__mro__instance.mro()来查看其MRO(Method Resolution Order 方法解析顺序)列表。对于上文代码中的类F的MRO如下:

结果即C3算法的解析结果。C3线性化算法我们就不去深究了(太深入),感兴趣的读者可以自己去了解一下,总的来说,一个类的MRO列表就是合并所有父类的 MRO 列表,并遵循以下三条原则:

  • 子类永远在父类前面。
  • 如果有多个父类,会根据它们在列表中的顺序被检查。
  • 如果对下一个类存在两个合法的选择,选择第一个父类。

同时为了解决多重继承中子类和父类有重复方法名的问题,Python 2.2之后引入了super函数。关于super,看Python类继承之super函数


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!