注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

Python面向对象:继承与多态

Python编程 彭东稳 3756次浏览 已收录 0个评论

一、继承与多态

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

简单来说继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写。另外在Python2.4开始,Python引入了object这个最上层的根类,但需要显式指定,也被称之为经典类;而在Python3中所有类默认都继承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是Python3内置用在类继承中的方法),可以使用super对象调用父类的方法及属性。再次实例化子类,如下:

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

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

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

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

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

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

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

再次运行,结果如下,会重写父类方法:

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

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

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

也为真,这说明不仅B是Sub1,同时也是Base。

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

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

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

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

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

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

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

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

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

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

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

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

二、多重继承

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

Python的类分为经典类与新式类。Python2.7之前的版本中可以采用经典类,经典类继承父类的顺序采用深度优先算法,但在Python3之后的版本就只承认新式类了。新式类在python2.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)or分享 (0)
关于作者:

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