【2020Python修炼记】面向对象编程——继承与派生
【目录】
一、继承介绍
二、继承与抽象
三、属性查找
四、继承的实现原理
1、菱形问题
2、继承原理
3、深度优先和广度优先
4、python Mixins机制
五、派生与方法重用
六、组合
一、继承介绍
1、什么是继承
(1)继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,
新建的类可称为子类或派生类,父类又可称为基类或超类
class ParentClass1: #定义父类 pass class ParentClass2: #定义父类 pass class SubClass1(ParentClass1): #单继承 pass class SubClass2(ParentClass1,ParentClass2): #多继承 pass
(2)查看类继承的所有父类:通过类的内置属性__bases__可以查看类继承的所有父类
>>> SubClass2.__bases__ (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
(3)经典类与新式类
-
在Python2中有经典类与新式类之分——
没有显式地继承object类的类,以及该类的子类,都是经典类;
显式地继承object的类,以及该类的子类,都是新式类。
>>> ParentClass1.__bases__ (<class ‘object'>,) >>> ParentClass2.__bases__ (<class 'object'>,)
-
而在Python3中,即使没有显式地继承object类,也会默认继承该类。
因而在python3中统一都是新式类,关于经典类与新式类的区别,未完待续。
提示:object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__
2、为什么要继承
为了解决类与类之间,代码冗余的问题
3、python中多继承的优缺点
# 优点:
子类可以同时遗传多个父类的属性,最大限度地重用代码
# 缺点:
# 1、违背人的思维习惯:继承表达的是一种什么"是"什么的关系
# 2、代码可读性会变差
# 3、不建议使用多继承,有可能会引发可恶的菱形问题,扩展性变差,
# 如果真的涉及到一个子类不可避免地要重用多个父类的属性,应该使用Mixins机制
二、继承与抽象
1、继承和抽象的理论方法
要找出类与类之间的继承关系,需要先抽象,再继承。
抽象即总结相似之处,总结对象之间的相似之处得到类(子类),
总结类与类之间的相似之处就可以得到父类。
2、如何实现继承
【栗子】
以学生和老师的信息管理为例,学生类与老师类之间存在信息参数冗余问题:
# 示范1:类与类之间存在冗余问题 class Student: school='OLDBOY' def __init__(self,name,age,sex): self.name=name self.age=age self.sex=sex def choose_course(self): print('学生%s 正在选课' %self.name) class Teacher: school='OLDBOY' def __init__(self,name,age,sex,salary,level): self.name=name self.age=age self.sex=sex self.salary=salary self.level=level def score(self): print('老师 %s 正在给学生打分' %self.name)
基于继承解决类与类之间的冗余问题:
# 示范2:基于继承解决类与类之间的冗余问题 class OldboyPeople: school = 'OLDBOY' def __init__(self, name, age, sex): self.name = name self.age = age self.sex = sex class Student(OldboyPeople): def choose_course(self): print('学生%s 正在选课' % self.name) stu_obj = Student('lili', 18, 'female') print(stu_obj.__dict__) print(stu_obj.school) stu_obj.choose_course() class Teacher(OldboyPeople): # 老师的空对象,'egon',18,'male',3000,10 def __init__(self, name, age, sex, salary, level): # 指名道姓地跟父类OldboyPeople去要__init__ OldboyPeople.__init__(self,name,age, sex) self.salary = salary self.level = level def score(self): print('老师 %s 正在给学生打分' % self.name) tea_obj=Teacher('egon',18,'male',3000,10) print(tea_obj.__dict__) print(tea_obj.school) tea_obj.score() # 输出结果: {'name': 'lili', 'age': 18, 'sex': 'female'} OLDBOY 学生lili 正在选课 {'name': 'egon', 'age': 18, 'sex': 'male', 'salary': 3000, 'level': 10} OLDBOY 老师 egon 正在给学生打分
三、属性查找
有了继承关系,对象在查找属性时,先从对象自己的__dict__中找,如果没有则去子类中找,然后再去父类中找……
1、单继承背景下的属性查找
【栗子1】
obj.f2()会在父类foo中找到f2,先打印 Foo.f2,
然后执行到self.f1(),即 obj.f1(),仍会按照:对象本身->类bar->父类foo的顺序依次找下去,
在类bar中找到f1,因而打印结果为 Bar.f1
# 示范一: class Foo: def f1(self): print('Foo.f1') def f2(self): print('Foo.f2') self.f1() # obj.f1() # 调用类Bar中的f1—— 先从自身对象找,没有再去自己的类Bar中有没有f1,没有再去继承的类Foo中找
class Bar(Foo): def f1(self): print('Bar.f1') obj=Bar() obj.f2() # 输出结果: # Foo.f2 # Bar.f1
【栗子2】如何实现子类和父类中,调用的 def f1(self) 不一样,即在自己类中,就只调用自己类中的 def f1(self)
方法一:Foo.f1(self) # 调用当前类Foo中的f1
# # 示范二: class Foo: def f1(self): print('Foo.f1') def f2(self): print('Foo.f2') Foo.f1(self) # 调用当前类Foo中的f1 class Bar(Foo): def f1(self): print('Bar.f1') obj=Bar() obj.f2() #输出结果: # Foo.f2 # Foo.f1
方法二:父类如果不想让子类覆盖自己的方法,可以采用双下划线开头的方式将方法设置为私有的
即 将父类中的 f1 隐藏起来,变为父类私有的
# 示范三: class Foo: def __f1(self): # _Foo__f1 print('Foo.f1') def f2(self): print('Foo.f2') self.__f1() # self._Foo__f1,# 调用当前类中的f1 class Bar(Foo): def __f1(self): # _Bar__f1 print('Bar.f1') obj=Bar() obj.f2() # 输出结果: # Foo.f2 # Foo.f1
2、多继承的属性查找
基本原则不变,但会涉及到菱形问题。详见后文分析。
四、继承的实现原理
1、菱形问题
大多数面向对象语言都不支持多继承,而在python中,一个子类是可以同时继承多个父类的,
这固然可以带来一个子类可以对多个不同父类加以重用的好处,
但也有可能引发著名的 diamond problem菱形问题(或称钻石问题,有时候也被称为“死亡钻石”),
菱形,其实就是对下面这种继承结构的形象比喻:
a类在顶部,b类和c类分别位于其下方,d类在底部将两者连接在一起形成菱形
这种继承结构下导致的问题称之为菱形问题:
如果a中有一个方法,b和/或c都重写了该方法,而d没有重写它,那么d继承的是哪个版本的方法:b的还是c的?如下所示:
class A(object): # 新式类 def test(self): print('from A') class B(A): def test(self): print('from B') class C(A): def test(self): print('from C') class D(B,C): pass obj = D() obj.test() # 结果为:from B
要想搞明白obj.test()是如何找到方法test的,需要了解python的继承实现原理——
(提前透露查找顺序——obj.test()的查找顺序是,先从对象obj本身的属性里找方法test,没有找到,则参照属性查找的发起者(即obj)所处类d的mro列表来依次检索,首先在类d中未找到,然后再b中找到方法test)
2、继承原理
【MRO】
python到底是如何实现继承的呢?
对于你定义的每一个类,python都会计算出一个方法解析顺序(mro)列表,该mro列表就是一个简单的所有基类的线性顺序列表,如下
>>> D.mro() # 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法 [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
python会在mro列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
而这个mro列表的构造是通过一个c3线性化算法来实现的。
这个算法实际上就是合并所有父类的mro列表并遵循如下三条准则:
1.子类会先于父类被检查
2.多个父类会根据它们在列表中的顺序被检查
3.如果对下一个类存在两个合法的选择,选择第一个父类
【PS】
1.由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照对象的类.mro()规定的顺序依次找下去
2.由类发起的属性查找,会按照当前类.mro()规定的顺序依次找下去
【举个栗子】以上个“菱形问题”的栗子为基础——
#【test1】 class A(object): def test(self): print('from A') class B(A): def test(self): print('from B') class C(A): def test(self): print('from C') class D(C,B): def test(self): print('from D') print(D.mro()) # 类D以及类D的对象访问属性都是参照该类的mro列表 # 输出结果: # [<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>] obj1 = D() obj1.test() # 输出结果:from D print(D.test) # 输出结果:<function D.test at 0x0000012EA27FA4C0> print(C.mro()) # 类C以及类C的对象访问属性都是参照该类的mro列表 # 输出结果:[<class '__main__.C'>, <class '__main__.A'>, <class 'object'>] obj2 = C() obj2.test() # 输出结果:from C print(C.test) # 输出结果: <function C.test at 0x0000022FA7D0A3A0> # 【test2】 class A(object): # def test(self): # print('from A') pass class B(A): def test(self): print('from B') class C(A): # def test(self): # print('from C') pass class D(C,B): # def test(self): # print('from D') pass print(D.mro()) # 类D以及类D的对象访问属性都是参照该类的mro列表 # 输出结果: # [<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>] obj1 = D() obj1.test() # 输出结果:from B print(D.test) # 输出结果:<function B.test at 0x00000246D6F3A310> # 总结:类相关的属性查找(类名.属性,该类的对象.属性),都是参照该类的mro
3、深度优先和广度优先
(1)非菱形继承
如果多继承是非菱形继承,经典类与新式类的属性查找顺序一样:都是一个分支一个分支地找下去,然后最后找object类
==栗子如下:
class E: # def test(self): # print('from E') pass class F: def test(self): print('from F') class B(E): # def test(self): # print('from B') pass class C(F): # def test(self): # print('from C') pass class D: def test(self): print('from D') class A(B, C, D): # def test(self): # print('from A') pass # 新式类 print(A.mro()) # A->B->E->C->F->D->object # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, # <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>] obj = A() obj.test() # 结果为:from F
(2)菱形继承
如果多继承是菱形继承,经典类与新式类的属性查找顺序不一样:
# 经典类:深度优先,会在检索第一条分支的时候就直接一条道走到黑,即会检索大脑袋(共同的父类)
# 新式类:广度优先,会在检索最后一条分支的时候检索大脑袋
==栗子如下:
经典类:深度优先——
新式类:广度优先——
class G: # 在python2中,未继承object的类及其子类,都是经典类 # def test(self): # print('from G') pass class E(G): # def test(self): # print('from E') pass class F(G): def test(self): print('from F') class B(E): # def test(self): # print('from B') pass class C(F): def test(self): print('from C') class D(G): def test(self): print('from D') class A(B,C,D): # def test(self): # print('from A') pass # # 新式类 print(A.mro()) # # A->B->E->C->F->D->G->object # # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, # # <class '__main__.D'>, <class '__main__.G'>, <class 'object'>] obj = A() obj.test() # 输出结果:from C # 经典类:A->B->E->G->C->F->D print(A.mro()) obj = A() obj.test() # 输出结果:from C
(3)总结
# 多继承到底要不用?
要用,但是规避几点问题——
# 1、继承结构尽量不要过于复杂
# 2、推荐使用mixins机制:在多继承的背景下满足继承的什么"是"什么的关系
4、python Mixins机制
五、派生与方法重用
六、组合
参考:
🐱不负韶华,只争朝夕🍚