18. 面向对象之继承、MIxin机制
继承 inheritance
# 1. 继承是一种创建新类的方式,新建的类可以继承一个或多个父类,新建的类可称为子类或派生类,父类又可称为基类或超类
# 子类可以通过继承获得父类的非私有属性和非私有方法,不需要自己重新写
# python支持多继承,而其他的语言不支持,在python中,新建的类可以继承一个或者多个类
# 子类能够获得父类的属性和方法,解决的是类与类之间代码的冗余的问题(类解决的时对象与对象之间代码冗余的问题)
class Parent1(object):
x=1111
class Parent2(object): # 默认所有的类都继承object类,可以不写
pass
class Sub1(Parent1): # 在括号中写上类就是继承,这种只写一个类的情况较单继承
pass
class Sub2(Parent1,Parent2): # 多继承
pass
print(Sub1.__bases__) # 该方法可以查看一个类的父类
print(Sub2.__bases__)
# print(Sub1.x)
# 2. 在python2中有经典类与新式类之分
# 新式类:继承了object类的子类,以及该子类的子类子子类......
# 经典类:没有继承object类的子类,以及该子类的子类子子类......
# 在python3中没有继承任何类,那么会默认继承object类,所以python3中所有的类都是新式类,在python2中是有区别的
# 3. python的多继承
# 优点:子类可以同时遗传多个父类的属性,最大限度地重用代码
# 缺点:要先进行归类,当类的关系复杂的时候会比较乱
# 多继承的情况下,代码的可读性会变差
# 不建议使用多继承,有可能会引发菱形问题,扩展性变差,
# 如果真的涉及到一个子类不可避免地要重用多个父类的属性,应该使用Mixin机制
# 4. 如何实现继承
## 示范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) # 初始化OldboyPeople类,得到初始化的OldboyPeople的属性
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()
# 5.单继承下的属性查找
## 示范一:
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() # 就是直接在对象里面找,找不到的时候再去类里面去找
## 示范二:
class Foo:
def f1(self):
print('Foo.f1')
def f2(self):
print('Foo.f2')
Foo.f1(self) # 调用当前类中的f1
class Bar(Foo):
def f1(self):
print('Bar.f1')
obj=Bar()
obj.f2()
## 示范三:
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()
菱形问题(也叫钻石问题、死亡钻石)
# 1. 菱形问题:就是一个类继承于多个类,被继承的类有都继承于一个非object的类
# 菱形继承问题详解:
class A():
def __init__(self):
print("进入A…")
print("离开A…")
class B(A):
def __init__(self):
print("进入B…")
A.__init__(self) # 能够直接访父类的属性,这是其中的一种方式
print("离开B…")
class C(A):
def __init__(self):
print("进入C…")
A.__init__(self)
print("离开C…")
class D(B, C):
def __init__(self):
print("进入D…")
B.__init__(self)
C.__init__(self)
print("离开D…")
d = D()
'''
运行结果
进入D…
进入B…
进入A…
离开A…
离开B…
进入C…
进入A…
离开A…
离开C…
离开D…
上面的B类和C类都继承了A类,D类既继承了B类又继承了C类,这就形成了一个菱形的继承关系
在创建D类对象时,A类的__init__方法执行了两遍,也就是说在D向上执行了B执行了A,然后又向上执行了C又执行了A,这显然是不合理的,最简单的一个例子是,如果A类里面存在一个计数器,这时就被执行了两遍
'''
# 解决初始化问题:super
# super的用法在python3和python2中存在一些区别,在2中super(类, 实例化的对象可直接传self).__init__(),在3中可以直接简写成super().__init__()
class A():
def __init__(self):
print("进入A…")
print("离开A…")
class B(A):
def __init__(self):
print("进入B…")
super(B,self).__init__() # 能够直接访父类的属性,这是其中的一种方式
print("离开B…")
class C(A):
def __init__(self):
print("进入C…")
super(C,self).__init__()
print("离开C…")
class D(B, C):
def __init__(self):
print("进入D…")
super(D,self).__init__()
print("离开D…")
d = D()
'''
运行结果
进入D…
进入C…
进入B…
进入A…
离开A…
离开B…
离开C…
离开D…
super方法的原理:
为了解决python中多继承出现的重复继承的问题,python使用了C3算法生成了MRO列表(Method Resolution Order方法解析顺序),当出现多重继承问题时,C3算法会计算出一个继承顺序,super方法正是按照MRO列表中的顺序来进行寻找的
'''
# 如何查看继承顺序——MRO列表
print(D.__mro__) # D这里指的是类名
# 需要注意的是,mro方法只能给类对象使用,实例对象不能使用
# 继承的原理:对于定义的类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表,第一个元素是类本身,接下来逐级向是上寻找父级类
print(D.mro()) # 类D以及类D的对象访问属性都是参照该类的mro列表,返回的结果是[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
# python会在MRO列表上从左到右开始查找基类(就是父类),直到找到第一个匹配这个属性的类为止。而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
# I.子类会先于父类被检查
# II.多个父类会根据它们在列表中的顺序被检查
# III.如果对下一个类存在两个合法的选择,选择第一个父类
obj = D()
obj.test()
print(D.test)
# 总结:类相关的属性查找(类名.属性,该类的对象.属性),都是参照该类的mro
# 2. 继承顺序:
## 如果多继承是非菱形继承,经典类与新式的属性查找顺序一样:都是一个分支一个分支地找下去,然后最后找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
obj = A()
obj.test() # 结果为:from F
## 如果多继承是菱形继承,经典类与新式类的属性查找顺序不一样:
# 经典类(python3):深度优先,会在检索第一条分支的时候就检索到头
# 新式类(python3):广度优先,会在检索最后一条分支的时候检索到头
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
# 经典类:A->B->E->G->C->F->D
obj = A()
obj.test()
# 总结:
# 多继承可以使用,但是规避几点问题:
# 1、继承结构尽量不要过于复杂
# 2、推荐使用mixin机制:在多继承的背景下满足继承的什么"是"什么的关系
新式类多继承广度优先,经典类多继承深度优先
多重继承、MRO 及 C3算法之间的关系
在计算机科学中,C3算法主要用于确定多重继承时,子类应该继承哪一个父类的方法,即方法解析顺序(Method Resolution Order,MRO)。
C3算法实现了三种重要特性:
- 保持继承拓扑图的一致性。
- 保证局部优先原则(比如A继承C,C继承B,那么A读取父类方法,应该优先使用C的方法而不是B的方法)。
- 保证单调性原则(即子类不改变父类的方法搜索顺序)。
为什么采用C3算法
C3主要用于在多继承时判断继承调用的路径(来自于哪个类)。在Python2.3之前是基于深度优先算法,为了解决原来基于深度优先搜索算法不满足本地优先级,和单调性以及继承不清晰的问题,从Python2.3起应用了新的C3算法。 在Python官网的The Python 2.3 Method Resolution Order中作者举了例子,说明这一情况。
F=type('Food', (), {remember2buy:'spam'})
E=type('Eggs', (F,), {remember2buy:'eggs'})
G=type('GoodFood', (F,E), {})
根据本地优先级在调用G类对象属性时应该优先查找F类,但是在Python2.3之前的算法给出的顺序是GEFO,而在C3算法中通过阻止类层次不清晰的声明来解决这一问题,以上声明在C3算法中就是非法的。
C3算法简介
判断mro要先确定一个线性序列,然后查找路径由由序列中类的顺序决定。所以C3算法就是生成一个线性序列。如果继承至一个基类:
class B(A)
这时B的mro序列为[B,A]
如果继承至多个基类:
class B(A1,A2,...,An)
这时B的mro序列:
mro(B) = [B] + merge(mro(A1), mro(A2),...,mro(An), [A1,A2,...,An])
merge操作就是C3算法的核心,是递归运算。
遍历执行merge操作的序列,如果一个序列的第一个元素,在其他序列中也是第一个元素,或不在其他序列出现,则从所有执行merge操作序列中删除这个元素,合并到当前的mro中。merge操作后的序列,递归地执行merge操作,直到merge操作的序列为空。
如果merge操作的序列无法为空,则说明不合法。
例子1:
class A(object):pass
class B(object):pass
class C(object):pass
class E(A,B):pass
class F(B,C):pass
class G(E,F):pass
A、B、C都继承至一个基类,所以mro序列依次为[A,O]、[B,O]、[C,O]
mro(E) = [E] + merge(mro(A), mro(B), [A,B])
= [E] + merge([A,O], [B,O], [A,B])
执行merge操作的序列为[A,O]、[B,O]、[A,B] A是序列[A,O]中的第一个元素,在序列[B,O]中不出现,在序列[A,B]中也是第一个元素,所以从执行merge操作的序列([A,O]、[B,O]、[A,B])中删除A,合并到当前mro,[E]中。
mro(E) = [E,A] + merge([O], [B,O], [B])
再执行merge操作,O是序列[O]中的第一个元素,但O在序列[B,O]中出现并且不是其中第一个元素。继续查看[B,O]的第一个元素B,B满足条件,所以从执行merge操作的序列中删除B,合并到[E, A]中。
mro(E) = [E,A,B] + merge([O], [O])
= [E,A,B,O]
同理
mro(F) = [F] + merge(mro(B), mro(C), [B,C])
= [F] + merge([B,O], [C,O], [B,C])
= [F,B] + merge([O], [C,O], [C])
= [F,B,C] + merge([O], [O])
= [F,B,C,O]
mro(G) = [G] + merge(mro[E], mro[F], [E,F])
= [G] + merge([E,A,B,O], [F,B,C,O], [E,F])
= [G,E] + merge([A,B,O], [F,B,C,O], [F])
= [G,E,A] + merge([B,O], [F,B,C,O], [F])
= [G,E,A,F] + merge([B,O], [B,C,O])
= [G,E,A,F,B] + merge([O], [C,O])
= [G,E,A,F,B,C] + merge([O], [O])
= [G,E,A,F,B,C,O]
Wiki有一个Python版本的C3算法:
def c3MRO(cls):
if cls is object:
# 讨论假设顶层基类为object,递归终止
return [object]
# 构造C3-MRO算法的总式,递归开始
mergeList = [c3MRO(baseCls) for baseCls in cls.__bases__]
mergeList.append(list(cls.__bases__))
mro = [cls] + merge(mergeList)
return mro
def merge(inLists):
if not inLists:
# 若合并的内容为空,返回空list
# 配合下文的排除空list操作,递归终止
return []
# 遍历要合并的mro
for mroList in inLists:
# 取head
head = mroList[0]
# 遍历要合并的mro(与外一层相同),检查尾中是否有head
### 此处也遍历了被取head的mro,严格地来说不符合标准算法实现
### 但按照多继承中地基础规则(一个类只能被继承一次),
### head不可能在自己地尾中,无影响,若标准实现,反而增加开销
for cmpList in inLists[inLists.index(mroList) + 1:]:
if head in cmpList[1:]:
break
else:
# 筛选出好head
nextList = []
for mergeItem in inLists:
if head in mergeItem:
mergeItem.remove(head)
if mergeItem:
# 排除空list
nextList.append(mergeItem)
# 递归开始
return [head] + merge(nextList)
else:
# 无好head,引发类型错误
raise TypeError
验证上述算法的正确性,
class A(object):pass
class B(object):pass
class C(object):pass
class E(A,B):pass
class F(B,C):pass
class G(E,F):pass
print([i.__name__ for i in c3MRO(G)])
## ['G', 'E', 'A', 'F', 'B', 'C', 'object']
在Python3下,如果想要查看继承顺序的话,更简单,每个类都有一个cls.mro()的方法。比如上面的例子,直接执行G.mro()就会打印MRO List。
例子2
再看一个复杂的例子:
class Type(type):
def __repr__(cls):
return cls.__name__
A = Type('A', (object,), {})
B = Type('B', (object,), {})
C = Type('C', (object,), {})
D = Type('D', (object,), {})
E = Type('E', (object,), {})
K1 = Type('K1', (A, B, C), {})
K2 = Type('K2', (D, B, E), {})
K3 = Type('K3', (D, A), {})
Z = Type('Z', (K1, K2,K3), {})
我们根据上面的继承关系可以画出继承图:

你可以尝试着自己计算一下MRO List,当然,最后你需要用上面的算法或者自带的.mro()进行验证。
print(Z.mro()) # [Z, K1, K2, K3, D, A, B, C, E, <class 'object'>]
print([i.__name__ for i in c3MRO(Z)])
# ['Z', 'K1', 'K2', 'K3', 'D', 'A', 'B', 'C', 'E', 'object']
看到这里,我想说的是,尽量不要乱用继承。否则,代码后期维护和升级会十分的困难,容易被后来者吐槽为“屎山”
mixin机制
# 多继承的正确使用方式:mixin机制
# mixin机制核心:就是在多继承背景下尽可能地提升多继承的可读性
class Vehicle:
pass
# 将交通工具的一些属性和功能封装在Vehicle类中,但是一些特殊的属性(比如交通工具大部分能够在陆地上行走,但是少数的交通工具能够飞)需要单独出来,但是又不能单独设立一个类,这是就使用到了Mixins机制
class FlyableMixin: # 加上一个Mixin后缀,表明这个类并不是一个单纯的类,而是一个提供一个属性或者功能,这是一种约定俗成的规范
def fly(self):
pass
class CivilAircraft(FlyableMixin,Vehicle): # 民航飞机
pass
class Helicopter(FlyableMixin,Vehicle): # 直升飞机
pass
class Car(Vehicle): # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
pass
# a. 使用Mixin类实现多重继承要注意
# b. 首先它必须表示某一种功能,而不是某个物品,python对于mixin类的命名方式一般是以Mixin,able,ible为后缀
# c. 其次它必须责任单一,一个Mixin类只相关一个特殊的性质(相关的功能可能不单一)
# d. 然后,它不依赖于子类的实现
# e. 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能(比如飞机照样可以载客,就是不能飞了)
子类中如何使用父类的属性
### 1. 直接访问,不依赖继承,也就是说可以随意使用其他类的属性
class OldboyPeople:
def __init__(self,name,age,sex):
self.name=name
self.age=age
self.sex=sex
def f1(self):
print('%s say hello' %self.name)
class Teacher(OldboyPeople):
def __init__(self,name,age,sex,level,salary):
OldboyPeople.__init__(self,name,age,sex)
self.level = level
self.salary=salary
tea_obj=Teacher('egon',18,'male',10,3000)
print(tea_obj.__dict__)
### 2. super()调用父类提供给自己的方法,依赖继承关系
# 调用super()会得到一个特殊的对象,该对象会参照继承时发起属性查找的那个类的mro,去当前类的父类中找属性,也就是说,super先调用发起的类的mro,然后使用当前类的父类
class OldboyPeople:
def __init__(self,name,age,sex):
self.name=name
self.age=age
self.sex=sex
def f1(self):
print('%s say hello' %self.name)
class Teacher(OldboyPeople):
def __init__(self,name,age,sex,level,salary):
# super(Teacher,self).__init__(name,age,sex) # python2中必须传参,比较麻烦super(类名,self)
super().__init__(name,age,sex) # 调用的是方法,自动传入对象,python3的用法,比较简单,super返回的是一个对象,所以不用传self
self.level = level
self.salary=salary
print(Teacher.mro())
tea_obj=Teacher('egon',18,'male',10,3000)
print(tea_obj.__dict__)
### 推荐使用super方法,不强制,但是注意不要混着用
### 调用super()会得到一个特殊的对象,该对象会参照继承时发起属性查找的那个类的mro,去当前类的父类中找属性,也就是说,super先调用发起的类的mro,然后使用当前类的父类
# 在下面的例子中,发起者是c,所以都是按照c的mro的顺序查找的
class A:
def test(self):
print('from A')
super().test1()
class B:
def test(self):
print('from B')
class C(A,B):
def test1(self):
print('from C')
obj=C()
obj.test()
print(C.mro())
组合
将一个类的对象封装到另一个累的对象的属性中,称为类的组合。组合与继承都是用来解决代码的重用性问题。不同的是:继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;而组合则是一种“有”的关系,比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合,如下示例
class Course:
def __init__(self,name,period,price):
self.name=name
self.period=period
self.price=price
def tell_info(self):
print('<%s %s %s>' %(self.name,self.period,self.price))
class Date:
def __init__(self,year,mon,day):
self.year=year
self.mon=mon
self.day=day
def tell_birth(self):
print('<%s-%s-%s>' %(self.year,self.mon,self.day))
class People:
school='清华大学'
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age
#Teacher类基于继承来重用People的代码,基于组合来重用Date类和Course类的代码
class Teacher(People): # 老师是人
def __init__(self,name,sex,age,title,year,mon,day):
super().__init__(name,age,sex)
self.birth=Date(year,mon,day) # 老师有生日
self.courses=[] # 老师有课程,可以在实例化后,往该列表中添加Course类的对象
def teach(self):
print('%s is teaching' %self.name)
python=Course('python','3mons',3000.0)
linux=Course('linux','5mons',5000.0)
teacher1=Teacher('lili','female',28,'博士生导师',1990,3,23)
# teacher1有两门课程
teacher1.courses.append(python)
teacher1.courses.append(linux)
# 重用Date类的功能
teacher1.birth.tell_birth()
# 重用Course类的功能
for obj in teacher1.courses:
obj.tell_info()

浙公网安备 33010602011771号