08-03 继承与派生
一. 继承介绍
1. 什么是继承?
"""
继承是一种创建新类的方式, 新创建的类可以继承一个或多个父类, 那么这时这个新建的类就被称之为子类或派生类, 父类就被成之为基类或超类, 且子类会遗传父类的属性.
注意: python支持多继承, 在python中新创建的类可以继承一个或多个父类(补充: python中支持多继承, 其它大部分语言都不支持.)
"""
class Parent1(object):
x = 1111
class Parent2(object):
pass
class Sub1(Parent1): # 单继承
pass
class Sub2(Parent1, Parent2): # 多继承
pass
# 查看当前类的所继承的父类: 通过内置属性"类名.__bases__"可以查看类继承的所有父类
print(Sub1.__bases__) # (<class '__main__.Parent1'>,)
print(Sub2.__bases__) # (<class '__main__.Parent1'>, <class '__main__.Parent2'>)
print(Sub2.x) # 1111
2. 经典类与新式类
# 强调!!: 在python2中有经典类与新式类之分. 在python3中没有继承任何类,那么会默认继承object类,所以python3中所有的类都是新式类
"""
新式类:显示继承object的类, 以及该类的子类类及子子类。。。
经典类:没有显示继承object的类, 以及该类的子类子及子类。。。
"""
3. python的多继承
# 不建议使用多继承, 有可能会引发可恶的菱形问题. 如果真的涉及到一个子类不可避免地重用多个父类的属性, 也不应该直接使用多继承, 而是应该使用Mixins机制.
"""
优点: 子类可以同时遗传多个父类的属性, 最大限度的解决类与类之间的代码重用问题
缺点: 继承应该表达的是一种什么"是"什么的关系. 违背了这种思维逻辑就会引发下面的问题.
1. 多继承违背了人的思维逻辑习惯
2. 代码的可读性会变差
3. 扩展性变差(可读性和扩展性是相辅相成的)
"""
二. 为何要用继承
# 类的作用: 解决对象与对象之间的代码重用性问题.
# 继承的作用: 解决类与类之间代码重用性问题.
三. 派生方法与重用(实现继承)
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. 基于继承解决类与类之间的冗余问题(派生的3种场景)
- 案例一: 子类直接继承父类所有
class People:
school = 'OLDBOY'
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex
class Student(People):
def choose_course(self):
print(f'学生{self.name}正在选课')
# 实例化学生对象
stu_obj = Student('egon', 19, 'male')
print(stu_obj.__dict__)
stu_obj.choose_course()
- 案例二: 子类在自己父类原有的基础之上派生成自己独有的
# 方式一:
"""
"指明道姓"调用继承的父类People, 调用类下的__init__(注意: 基于类调用这是这是一个普通函数, 应该准守函数的传参规则.)
"""
class Teacher(People):
def __init__(self, name, age, sex, salary, level):
People.__init__(self, name, age, sex)
self.salary = salary
self.level = level
def score(self):
print(f'老师{self.name}正在给学生打分')
# 实例化老师对象
tea_obj = Teacher('tank', 20, 'male', 30000, 9)
print(tea_obj.__dict__)
tea_obj.score()
# 方式二: super()
"""
调用super()会得到一个特殊的对象, 该对象专门用来引用父类的属性, 会严格参照属性发起者类的MRO, 并去当前类的父类中查找. (注意: 调用的是绑定方法, 会自动传入self)
"""
class People:
school = 'OLDBOY'
def __init__(self, name, age, sex):
self.name = name
self.age = age
self.sex = sex
class Teacher(People):
def __init__(self, name, age, sex, salary, level):
# super(Teacher, self).__init__(name, age, sex) # python2中的super()的使用需要完整的写成super(自己的类名, self), 而在python3中可以简写为super()
super().__init__(name, age, sex)
self.salary = salary
self.level = level
def score(self):
print(f'老师{self.name}正在给学生打分')
# 实例化老师对象
tea_obj = Teacher('tank', 20, 'male', 30000, 9)
print(tea_obj.__dict__)
tea_obj.score()
# super()案例
class A:
def test(self):
print('from A')
super().test() # 去当前"obj"类的父类"B"中去找
class B:
def test(self):
print('from B')
class C(A, B):
pass
obj = C()
obj.test()
# 参照属性发起者"obj"类的MRO: C -> A -> B -> object
print(C.mro())
'''
from A
from B
'''
# 总结
"""
方式一与方式二的区别:
方式一: 没有继承关系
方式二: super()依赖与继承, 并且没有直接继承关系, super()只会按照MRO规定的列表顺序往后查找.
推荐使用:
在子类中重用父类功能的这两种方式, 使用任何一种都可以, 但是在最新的代码中还是推荐使用super()
"""
四. 属性查找
# 强调!!: 谁来调用, 查找方式就以谁作为起始位置开始查找
"""
由对象发起的属性查找: 会从对象自身的属性里检索, 没有则会按照对象的类"类名.mro()"规定的顺序依次找下去
由类发起的属性查找: 会按照当前类"类名.mro()"规定的顺序依次找下去
"""
class Foo:
def f1(self):
print('Foo.f1')
def f2(self):
print('Foo.f2')
self.f1() # obj.f1() # "obj"来调用, 执行查找方式就以"obj"作为起始位置开始查找
class Bar(Foo):
def f1(self):
print('Bar.f1')
obj = Bar()
# 查找顺序: obj.f2 -> Bar.f2 -> Foo.f2 --> Foo中找到了f2 -> obj.f1 -> Bar.f1 -> Bar中找到了f1
obj.f2()
'''
Foo.f2
Bar.f1
'''
五. 父类如果不想让子类覆盖自己的方法的2种实现方式
1. 可以采用"指明道姓"的方式访问自己内部的方法
class Foo:
def f1(self):
print('Foo.f1')
def f2(self):
print('Foo.f2')
Foo.f1(self) # 采用"指明道姓Foo"的方式访问自己内部的方法
class Bar(Foo):
def f1(self):
print('Bar.f1')
obj = Bar()
# 查找顺序: obj.f2 -> Bar.f2 -> Foo.f2 --> Foo中找到了f2 -> Foo.f1 --> Foo中找到了f1
obj.f2()
"""
Foo.f2
Foo.f1
"""
2. 可以采用封装的思想使用双下划线开头的方式将方法设置为私有的.
class Foo:
def __f1(self): # _Foo__f1
print('Foo.f1')
def f2(self):
print('Foo.f2')
self.__f1() # self._Foo__f1 # 采用封装的思想使用双下划线开头的方式将方法设置为私有
class Bar(Foo):
def __f1(self): # _Bar__f1
print('Bar.f1')
obj = Bar()
# 查找顺序: obj.f2 -> Bar.f2 -> Foo.f2 --> Foo中找到了f2 -> obj._Foo__f1 --> Bar._Foo__f1 --> Foo._Foo__f1 --> Foo中找到了_Foo__f1
obj.f2()
"""
Foo.f2
Foo.f1
"""
六. 多继承带来的菱形问题
前言补充: 大多数面向对象语言都不支持多继承, 而在python中是可以同时继承多个父类的.这样固然可以带来一个子类可以对多个不同的父类加以重用的好处, 但也有可能引发著名的菱形问题(Diamond problem 或称钻石问题, 有时候也被称之为"死亡钻石"
1. MRO介绍
"""
对于你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表, python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。而这个MRO列表的构造是通过一个C3线性化算法来实现的.
MRO列表之继承查找顺序的三条准则:
1. 子类会先于父类被检查
2. 多个父类会根据它们在列表中的顺序被检查
3. 如果对下一个类存在两个合法的选择,选择第一个父
"""
2. 菱形问题
"""
什么是菱形问题?
多个子类最终会继承到一个非object类中, 这种继承关系就会引发菱形问题.
"""
class A(object):
def test(self):
print('from A')
pass
class B(A):
# def test(self):
# print('from B')
pass
class C(A):
# def test(self):
# print('from C')
pass
class D(C, B):
# def test(self):
# print('from D')
pass
# 类D以及类D的对象访问属性都是参照该类的mro列表
print(D.mro()) # [<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
d_obj = D()
d_obj.test()
print(D.test)
# 类C以及类C的对象访问属性都是参照该类的mro列表
print(C.mro()) # [<class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
c_obj = C()
c_obj.test()
3. 如果多继承是非菱形继承,经典类与新式的属性查找顺序一样
"""
查找顺序: 经典类与新式类的属性查找顺序都是按照类括号中继承类的先后顺序依次按照一个分支一个分支的查找下去(注意: 每个分支的最终继承的非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
4. 如果多继承是菱形继承,经典类与新式类的属性查找顺序不一样
# 强调!!: 只有python2中有经典类的概念, python3中默认显示继承或不继承object都是新式类.
"""
查找顺序: 经典类深度优先, 新式类广度优先
深度优先与广度优先都是基于当前分支最终继承的非object类当作参考点. 如果是按照新式类则会在直接在第一分支下就找最终继承的非object类. 如果是广度优先则会在最后分支中才去找最终继承的非object类.
提示: python2中没有继承object的类及其子类都没有"类名.mro()"方法, 只有显示继承object类才有. 而python3因为无论有没有显示继承都是默认继承object类, 所以都有"类名.mro()"方法.
"""
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
# 经典类: 要放到python2中执行.
# print(A.mro()) # A -> B -> E -> G -> C -> F -> D
obj = A()
obj.test() # 打印结果: python2中"from G". python3中"from C"
5. 总结: 多继承到底要不用?
"""
要用, 但是规避几点问题:
1. 继承结构尽量不要过于复杂
2. 推荐使用mixins机制, 这才是多继承环境下的正确打开方式, 核心思想就是在多继承的背景下满足继承的什么"是"什么的关系.
"""
七. MixIns机制: 本质还是基于多继承
"""
MixIns机制核心介绍:
为了能在多继承的背景下, 尽可能地提升多继承的可读性. python提供了一种基于类的命名方式, 它是通过定制一种命名规范来达到一种提示性的效果, 从而让多继承满足人的思维习惯. 明确什么"是"什么的关系. (提示: 变量的另一种规范 --> 常量)
提示: 它不是表达什么"是"什么的关系, 只是明确这是一个混合的关系, 而不是一个公共的父类, 而是混合的临时掺杂的类, 具有一定局限性.
MixIns机制的命名规范:
python对于mixin类的命名方式一般以 MixIn, able, ible 为后缀. 不过推荐还是使用MixIn作为标识, 寓意更加明确.
MixIns机制所带来的的好处:
因为多继承带来的缺点, 使用MixIns机制可以变相的优化程序的可读性和可维护性以及可扩展性.
"""
class Vehicle: # Vehicle 交通工具
pass
class FlyableMixIn: # Flyable 飞行器
def fly(self):
pass
# 表达什么"是"什么的关系的类"Vehicle", 一定要放在结尾
class CivilAircraft(FlyableMixIn, Vehicle): # CivilAircraft 民航飞机
pass
class Helicopter(FlyableMixIn, Vehicle): # Helicopter 直升飞机
pass
class Car(Vehicle): # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
pass
八. 组合
"""
什么是组合?
在一个类中以另外一个类实例化的对象作为数据属性, 称之为类的组合.
组合的作用:
用来解决代码重用性问题
组合和继承的区别:
继承是一种"是"的关系. 比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承
组合是一种"有"的关系. 比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合
"""
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集对象独有的属性、Teacher类中的内容、Course类中的内容于一身(都可以访问到),是一个高度整合的产物
# teacher1有两门课程
teacher1.courses.append(python)
teacher1.courses.append(linux)
# 重用Date类的tell_birth功能
teacher1.birth.tell_birth()
# 重用Course类的tell_info功能
for obj in teacher1.courses:
obj.tell_info()