面向对象三大特性之继承
继承
继承是面向对象思想的另一个特性。它的存在是为了解决类与类之间代码重复的问题。
class Student:
school_name = 'oldboy'
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex
def select_course(self):
print(f'学生:{self.name}正在选课。。。')
class Teacher:
school_name = 'oldboy'
def __init__(self, name, age, sex, level):
self.name = name
self.age = age
self.sex = sex
self.level = level
def score(self):
print(f'老师:{self.name}正在打分。。。')
上述两个类之间有大量重复的代码,我们现在介绍的这个继承就是为了解决这个代码重复的问题。
继承是一种创建新类的方式,如果基于一个已经存在的类A而创建了新类B。则新类B可称为子类或派生类,类A又可称为基类或超类,子类会遗传父类的属性。
需要注意的是:python支持多继承。在Python中,新建的类可以继承一个或多个父类。
class ParentClass1: #定义父类
pass
class ParentClass2: #定义父类
pass
class SubClass1(ParentClass1): #单继承
pass
class SubClass2(ParentClass1,ParentClass2): #多继承
pass
# python2中有经典类与新式类之分
- 经典类:没有继承object类的子类,以及该子类的子类子子类。。。
- 新式类:继承了object类的子类,以及该子类的子类子子类。。。
# 在python3中没有继承任何类,那么会默认继承object类,所以python3中所有的类都是新式类
# 提示:object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__
# 通过类的内置属性__bases__可以查看类继承的所有父类
现实中是先有对象,再抽象出不同的类。如,人、动物。再在类的基础上继续抽象。
编程中则相反,先有父类再有子类。如果,类与类之间的关系为:什么'是'什么的关系(比如人类,猪类,猴类都是动物类)。则可以使用继承创建新类。子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。将学生类和老师类再抽象出一个人类,在人类里面写大家共有的代码,则每个子类内部写自己特有的代码,则解决了类与类之间代码重复的问题。
class OldBoyPeople(object):
school = 'oldboy'
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex
class Student(OldBoyPeople):
def select_course(self):
print(f'学生:{self.name} 正在选课。。。')
class Teacher(OldBoyPeople):
def __init__(self, name, age, sex, level):
OldBoyPeople.__init__(self, name, age, sex) # '指名道姓'调用父类的这个方法,需要手动传第一个参数
self.level = level
def score(self):
print(f'老师:{self.name} 正在打分。。。')
stu_obj = Student('lili', 18, 'female', 1001) # Student类并没有__init__方法,会使用父类的
print(stu_obj.__dict__)
python的多继承
优点:子类可以同时遗传多个父类的属性,最大限度地重用代码
缺点:
1、违背人的思维习惯:继承表达的是一种什么"是"什么的关系
2、代码可读性会变差
3、不建议使用多继承,有可能会引发可恶的菱形问题,扩展性变差,
如果真的涉及到一个子类不可避免地要重用多个父类的属性,应该使用Mixins
属性查找
本部分只谈单继承情况下的属性查找。下一部分详细谈多继承情况下的属性查找。
有了继承关系对象在查找属性时,先从对象自己的__dict__中找,如果没有则去对象所在类中找,然后再去父类中找……
案例1:
class Foo:
def f1(self):
print('Foo.f1')
def f2(self):
print('Foo.f2')
self.f1() # obj.f1()
class Bar(Foo):
def f1(self):
print('Bar.f1')
obj=Bar()
obj.f2()
# obj.f2()会在父类Foo中找到f2,先打印Foo.f2,然后执行到self.f1(),即obj.f1(),仍会按照:对象本身->类Bar->父类Foo的顺序依次找下去,在类Bar中找到f1,因而打印结果为Foo.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()
继承的实现原理
菱形问题
大多数面向对象编程语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的。这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发著名的菱形问题(或称钻石问题,有时候也被称为“死亡钻石”)。菱形其实就是对下面这种继承结构的形象比喻。需要注意,这种结构中A不包括object类。
这种结构的继承关系将有可能导致菱形问题。要理解下面的执行结果,需要了解python的继承实现原理。
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
继承原理
这个继承原理其实就是在这种多继承的情况下,python到底按照什么原理来实现属性查找的。
# 你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有类的线性顺序列表
# 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法
D.mro() # 得到一个属性查找顺序列表
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
Python会在MRO列表上从左到右开始查找,直到找到第一个匹配这个属性的类为止。 而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理。
所以obj.test()的查找顺序:先从对象obj本身的属性里找方法test,没有找到,则参照属性查找的发起者(即obj)所处类D的MRO列表来依次检索,首先在类D中未找到,然后再B中找到方法test
总结:从对象或者对象所在类的位置开始查找,找不到就从所在类在MRO列表中的位置往右找。。。
1.由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照对象的类.mro()规定的顺序依次找下去,
2.由类发起的属性查找,会按照当前类.mro()规定的顺序依次找下去
深度优先&广度优先
如果多继承结构为非菱形结构,此时,会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性。经典类和新式类的查找顺序相同,从左到右一条分支一条分支查。只不过新式类最后会多查找一个object类。
print(A.mro())
#
[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>]
如果继承关系为菱形结构,那么经典类与新式类会有不同MRO,分别对应属性的两种查找方式:深度优先和广度优先
经典类的深度优先:查找顺序为,obj->A->B->E->G->C->F->D
新式类的广度优先:查找顺序为,obj->A->B->E->C->F->D->G->object
Mixins机制
多继承的正确打开方式:Mixins。Mixins核心机制:就是在多继承的背景下尽可能地提升多继承的可读性。通过命名规范来满足多继承满足人的思维习惯什么是什么的关系
class Vehicle: # 交通工具
pass
class FlyableMixin:
def fly(self):
print("I am flying")
class CivilAircraft(FlyableMixin, Vehicle): # 民航飞机
pass
class Helicopter(FlyableMixin, Vehicle): # 直升飞机
pass
class Car(Vehicle): # 汽车
pass
# ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路
可以看到,上面的CivilAircraft、Helicopter类实现了多继承,不过它继承的第一个类我们起名为FlyableMixin,而不是Flyable,这个并不影响功能。但是会告诉后来读代码的人,这个类是一个Mixin类,表示混入(mix-in),这种命名方式就是用来明确地告诉别人(python语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类。所以从含义上理解,CivilAircraft、Helicopter类都只是一个Vehicle,而不是一个飞行器。
使用Mixin类实现多重继承要非常小心
- 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀。
- 其次它必须责任单一,如果有多个功能,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类。
- 它不依赖于子类的实现;子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。
- 通常Mixin的类放在子类括号内的左边。
Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性也是会越来越差的。所以,这种机制其实只是在一定程度上,避免程序因为使用多继承导致的可读性变差。因此,对于多继承的使用不推荐继承的结构过于复杂。
派生&方法重用
子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找。
如果,子类的某一个属性和父类的属性完全一样,则会覆盖父类的属性,优先使用自己的。
如果,子类的想使用父类的某个属性,但又是在父类的基础上扩展了新功能,则派生父类的方法。
派生的方式有两种:第一种为指名道姓的调用父类的属性;第二种为super()的调用方式
第一种和继承没有任何关系,只是函数的普通调用而已。
第二种是按照继承关系调用的。遇到super()属性,就会按照属性查找的发起者的MRO列表里面的下一个类里面找需要查找的属性。
>>> # A没有继承B
... class A:
... def test(self):
... super().test()
...
>>> class B:
... def test(self):
... print('from B')
...
>>> class C(A,B):
... pass
...
>>> C.mro() # 在代码层面A并不是B的子类,但从MRO列表来看,属性查找时,就是按照顺序C->A->B->object,B就相当于A的“父类”
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,<class ‘object'>]
>>> obj=C()
>>> obj.test() # 属性查找的发起者是类C的对象obj,所以中途发生的属性查找都是参照C.mro()
from B
补充:super()是给新式类使用的。python2中使用:super(类名, self).属性;python3可以简化为super().属性
>>> class Teacher(People):
... def __init__(self,name,sex,age,title):
... super().__init__(name,age,sex)
# 调用的是绑定方法,自动传入self,python2:super(Teacher, self).__init__(name, age, sex)
... self.title=title
... def teach(self):
... print('%s is teaching' %self.name)
...
组合
在一个类中以另外一个类的对象作为数据属性,称为类的组合。
组合与继承都是用来解决代码的重用性问题。
继承是一种“是”的关系,比如老师是人、学生是人。
组合则是一种“有”的关系,比如老师有生日,老师有多门课程。
class Student:
def __init__(name, age):
self.name = name
self.age = age
class Coursr:
pass
stu_obj = Student('jack', 18)
course_obj = Course()
stu_obj.course = course_obj