2. 继承
面向对象三要素之一,继承Inheritance
人类和猫类都继承自动物。
个体继承自父母,继承了父母的一部分特征,但也可以有自己的个性。
在面向对象的世界中,从父类继承就可以直接拥有父类的属性和方法,这样就减少代码、多复用。
子类也可以定义自己的属性和方法。
看一个不用继承的例子
上面的2个类虽然有关系,但是定义时并没有建立这种关系,而是各自完成定义。
动物类和猫类都会叫,但是它们的叫法有区别,所以分别定义。
上例可以看出,通过继承,猫类不用写代码了,直接继承了父类的属性和方法。
继承可以让子类从父类获取特征(属性和方法),子类不用再写了可以直接使用,达到了复用的目的。
- 继承
- class Cat(Animal)这种形式就是从父类继承,括号中写上继承的类的列表。
- 继承可以让子类从父类获取特征(属性和方法)
- 父类
- Animal就是Cat的父类,也称为基类、超类。
- 子类
- Cat就是Animal的子类,也称为派生类
2.1 定义
格式如下
如果类定义时,没有基类列表,等同于继承自object。在Python3中,object类是所有对象的根基类(object是Python中继承这条线,所有对象的祖先,称为根基类)
注意,上例在Python2中,两种写法是不同的。
Python支持多继承,继承也可以升级。
查看继承的特殊属性和方法有
特殊属性和方法 | 含义 |
__bases__ | 类的基类元组 |
__base__ | 类的基类元组的第一项 |
__mro__ | 显示方法查看顺序,基类的元组 |
mro()方法 | 同上,返回列表 |
__subclasses__ | 类的子类列表 |
2.2 继承中的访问控制
从父类继承,自己没有,就可以到父类中找
私有的都是不可以访问的,但是本质上依然是改变了名称放在这个属性所在类或实例的的__dict__中,知道这个新名称就可以直接找到这个隐藏的变量,这是个黑魔法技巧,慎用。
总结:
继承时,公有成员,子类和实例都可以随意访问;私有成员被隐藏,子类和实例不可以直接访问,但私有变量所在的类内的方法中可以访问这个私有变量。
Python通过自己一套实现,实现和其他语言一样的面向对象的继承机制。
实例属性查询顺序(重点!!!)
实例的__dict__
—> 类__dict__
—>如果有继承—>父类__dict__
如果搜索这些地方后没有找到就会抛异常,先找到就立即返回了。
2.3 方法的重写、覆盖override
Cat中能覆盖自己的方法吗?即Cat中编写两个shout方法,下面的shout能覆盖上面一个shout方法吗?答案是可以的。
Cat中能否对父类方法做个增强,不需要完全重写?
super()可以访问到父类的类属性。
那对于类方法和静态方法呢?
静态方法和类方法,是特殊方法,也是类属性,这些方法都可以实现覆盖,原理都一样,属性字典的搜索顺序
2.4 继承时使用初始化
如果子类中没有__init__方法,就会继承父类的__init__方法,那如果子类和父类同时都存在__init__方法,会怎样呢
上面的案例中父类的__init__并没有被子类继承,而是被子类的__init__方法覆盖了,能不能既可以用子类的__init__方法实例化的同时调用父类的__init__呢,是可以的,用super方法
而且作为好习惯,如果在父类中定义了__init__方法,你就该在子类中调用它。
在子类没有__init__方法的时候,子类会自动调用父类的__init__方法。在子类和父类都有__init__方法时,就需要你通过super方法手动调用父类的__init__方法了。
super().__init__(b+h, b-h)
只有两个参数,而且开头并没有self参数,这是因为super()等同于super(Cat, self),那么super().__init__(b+h, b-h)
也可以写成Animal.__init__(self, b+h, b-h)
同时需要注意的是,调用父类的方法前后顺序是很重要的,顺序不同,结果可能也不同。
规范写法是super.__init__(name, age)写在前面,父类方法打好基础,子类做增强即可。注意,如果super方法写在后面,则self.age += 2需要改写成self.age = age + 2,想想这是为什么?
那么直接将上面例子中实例属性改成私有变量呢?
上例中,打印10,原因看__dict__就知道了。因为父类Animal的show方法中__age会被解释为_Animal__age,因此显示的是11,不是13.
这样的设计不好,Cat的实例c应该显示自己的属性值更好。
解决办法:一个原则,自己的私有属性,就该自己的方法读取和修改,不要借助其他类的方法,即使是父类或者派生类的方法。
2.5 单继承
上面的例子中,类的继承列表中只有一个类,这种继承称为单一继承。
OCP原则:多用“继承”、少修改
继承的用途:在子类上实现对基类的增强,实现多态。
开闭原则(OCP,The Open-Closed Principle)两个主要特征:(1)对扩展开放(open for extension):模块的行为的可以扩展的,当应用的需求改变时,可以对模块进行扩展。(2)对修改关闭(closed for modification):对模块进行扩展时,不必改动模块的源代码
2.6 多继承
一个类继承自多个类就是多继承,他将具有多个类的特征。
2.6.1 多继承弊端
多继承很好的模拟了世界,因为事物很少是单一继承,但是舍弃简单,必然引入复杂性,带来了冲突。如同一个孩子继承了来自父母双方的特征。那么到底眼睛像爸爸还是妈妈呢?孩子究竟该像谁多一点呢?
多继承的实现会导致编译器设计的复杂度增加,所以有些高级语言舍弃了类的多继承。
C++支持多继承;Java舍弃了多继承
Java中,一个类可以实现多个接口,一个接口也可以继承多个接口。Java的接口很纯粹,只是方法的声明,继承者必须实现这些方法,就具备了这些能力,就能干什么。
多继承可能会带来二义性,例如,猫和狗都继承自动物类,现在如果一个类多继承了猫和狗类,猫和狗都有shout方法,子类究竟继承谁的shout呢?
解决方案:
实现多继承的语言,要解决二义性,深度优先或者广度优先
2.6.2 多继承实现
左图是多继承(菱形继承,图结构),右图是单一继承(线性结构)
多继承带来路径选择问题,究竟继承哪个父类的特征呢?
Python使用MRO(method resolution order方法解析顺序)解决基类搜索顺序问题
- 历史原因,MRO有三个搜索算法:
- 经典算法:按照定义从左到右,深度优先策略。2.2版本之前左图的MRO是MyClass,D,B,A,C,A
- 新式类算法:是经典算法的升级,深度优先,重复的只保留最后一个,2.2版本左图的MRO是MyClass,D,B,C,A,object
- C3算法:在类被创建出来的时候,就计算出了一个MRO有序列表。2.3之后支持,Python3唯一支持的算法,左图中MRO是MyClass,D,B,C,A,object。C3算法解决了多继承的二义性。
经典算法有很大的问题,如果C中有方法覆盖了A的方法,也不会访问到C的方法,因为先访问A的(深度优先)
新式类算法,依然采用了深度优先,解决了重复问题,但是同经典算法一样,没有解决继承的单调性。
C3算法,解决了继承的单调性,它阻止创建之前版本产生二义性的代码。求得的MRO本质是为了线性化,且确定了顺序。
单调性:假设A、B、C三个类,C的MRO是[C, A, B],那么C的子类的MRO中,A、B的顺序一致就是单调的
2.6.3 多继承的缺点
当类很多且继承复杂的情况下,继承路径太多,很难说清什么样的继承路径
Python语法是允许多继承的,但Python代码是解释执行,只有执行的时候,才发现错误。
团队协作开发,如果引入多继承,那么代码很有可能不可控。
不管编程语言是否支持多继承,都应当避免多继承。
Python的面向对象,我们看到的太灵活了,太开放了,所以要团队守规矩。
2.6.4 Mixin
类有下面的继承关系
文档Document类是其他所有文档类的抽象基类;
Word、Pdf类是Document的子类。
需求:为Document子类提供打印能力
思路:
1、在Document中提供print方法
假设已经有了下面三个类
基类提供的方法可以不具体实现,因为它未必适合子类的打印,子类中需要覆盖重写。基类中定义,不实现的方法,称为“抽象方法”。在Python中,如果采用这种方式定义的抽象方法,子类可以不实现,直到子类使用该方法的时候才报错。
print算一种能力——打印能力,不是所有的Document的子类都需要的,所以从这个角度出发,这上面的基类Document设计是有问题的。
2、需要打印的子类上增加
如果在所有的子类Word或Pdf上直接增加,虽然可以,却违反了OCP原则,所以可以继承后增加打印功能。因此有下图:
看似不错,如果需要还要提供其他功能,如何继承?
例如,如果该类用于网络,还应具备序列化的能力,在类上就应该实现序列化
可序列化还可能分为使用pickle、json、messagepack等
这个时候发现,为了增加一种能力,就需要一次继承,类可能太多了,继承的方式不是很好了。
可提供的功能太多,A类需要某几种功能,B类需要另几样功能,它们需要的是多种功能的自由组合,继承实现很繁琐。
3、装饰器
用装饰器增强一个类,把功能给类附加上去,那个类需要,就装饰它。
优点:简单方便,在需要的地方动态增加,直接使用装饰器。可以为类灵活的增加功能
4、Mixin
先看代码
Mixin就是其它类混合进来的,同时带来了类的属性和方法
这里看来Mixin类和装饰器效果一样,也没有什么特别的。但是Minxin是类,就可以继承。
Mixin类
Mixin本质上就是多继承实现的。
Mixin体现的是一种组合的设计模式。
在面向对象的设计中,一个复杂的类,往往需要很多的功能,而这些功能有来自不同的类提供这就需要从设计模式的角度来说,多组合,少继承
Mixin类的使用原则:
- Mixin类中不应该显示的出现__init__初始化方法
- Mixin类通常不能独立工作,因为它是准备混入别的类中部分功能实现
- Mixin类的祖先类也应该是Mixin类
使用时,Mixin类通常在继承列表的第一个位置
Mixin类和装饰器,这两种方式都可以使用,看个人喜好。
2.7 抽象类(接口)
在设计父类的方法时, 父类方法只定义方法名, 然后用pass语句来代替具体实现代码. 继承的子类重写该方法, 并给出具体的实现方法.
如此设计的父类即为抽象类(接口)
- 抽象方法: 方法体是空实现的(pass)称之为抽象方法
- 抽象类: 含有抽象方法的类称之为抽象类
抽象类多用做顶层设计(设计标准), 以便子类做具体实现, 同时也是对子类的一种软性约束, 要求子类必须复写(实现)父类的一些方法.
同时, 可以配合多态使用, 获得不同的工作状态.通过多态语法, 实现模块和模块之间的解耦合.
3. 多态
3.1 多态定义
在面向对象过程中,父类、子类通过继承联系在一起,如果可以通过一套方法,在不同的场景下就可以实现不同表现,就是多态。
上例中,同一接口shout,对于不同类型实例,调用同一个方法,具有不同的表现,这就是多态的概念。
多态实现的三个前提:
- 必须有子类,继承
- 子类中实现对父类方法的 覆盖
- 父类引用指向子类实例
3.2 多态的应用场景
父类型可以作为函数的形参类型, 这样可以接受其任意的子类对象, 实现传入什么(子类对象), 就调用该子类具体的功能, 起到了隐藏接口差异性的作用.(这也是多态中父类引用指向子类实例的体现.)
不使用多态思想实现
使用多态思想实现
- 在不改变框架代码的情况下,通过多态语法轻松的实现模块和模块之间的解耦合;实现了软件系统的可拓展
- 对解耦合的大白话解释:搭建的平台函数def object_play(herofighter:HeroFighter, enemyfighter:EnemyFighter) 相当于任务的调用者;子类、孙子类重写父类的函数,相当于子任务;相当于任务的调用者和任务的编写者进行了解耦合
- 对可拓展的大白话解释: 搭建的平台函数def object_play(herofighter:HeroFighter, enemyfighter:EnemyFighter),在不做任何修改的情况下,可以调用后来人写的代码
- 对“继承和多态对照理解”大白话解释:
- 继承相当于:孩子可以复用老爹的东西。
- 多态相当于:老爹框架,不做任何修改的情况下,可以可拓展的使用后来人(孩子)写的东西。
4. 作业
1、 shape基类要求所有子类都必须提供面积的计算,子类有三角形、矩形、圆。(三角形面积——海伦公式)
2、上题圆类的数据可序列化
3、用面向对象实现Linkedlist链表
- 单项列表实现append、iternodes方法
- 双向列表实现append、iternodes、pop、insert、remove方法
参考资料:《Python编程:从入门到实践》
《Python学习手册》(上下册)
某哥教育Python全栈开发资源