Python-面向对象编程之继承
一、基本概念
什么是继承
继承
是一种创建新类
的方式,新建的类
可称为子类
或派生类
,被继承的类
称为父类
,父类又可称为基类或超类,子类会遗传
父类的属性。
需要注意的是:python支持多继承
,在Python中,新建的类可以继承一个或多个父类。
继承的形式:
class ParentClass1: # 定义父类1
pass
class ParentClass2: # 定义父类2
pass
class SubClass1(ParentClass1): # 子类1 -> 单继承
pass
class SubClass2(ParentClass1,ParentClass2): #子类2 -> 多继承
pass
通过类的内置属性__bases__
可以查看类继承的所有父类
:
print(SubClass2.__bases__)
# (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
PS: 在python2中有新式类
与经典类
之分
新式类:继承了
object
类(python内置的一种类)的子类,以及该子类的子类子子类……
经典类:没有继承object
类的子类,以及该子类的子类子子类……
在python3中没有继承任何类的类,那么会默认继承object
类,所以python3中所有的类都是新式类
。
class Parent1:
pass
class Parent2:
pass
print(Parent1.__bases__) # (<class 'object'>,)
print(Parent2.__bases__) # (<class 'object'>,)
为何要用继承
用来解决类与类
之间代码冗余问题
class ParentClass1: # 定义父类
x = 1
class ParentClass2: # 定义父类
y = 2
class SubClass(ParentClass1, ParentClass2): # 多继承
pass
obj = SubClass()
print(obj.x, obj.y) # 1 2
注意:python的多继承
- 优点:子类可以同时
遗传
多个父类的属性
,最大限度地重用代码 - 缺点:
- 代码可读性会变差
- 不建议使用多继承,有可能会引发可恶的
菱形问题
,扩展性变差。如果真的涉及到一个子类不可避免地要重用多个父类的属性,应该使用Mixins
机制
二、继承的实现
要找出类与类之间的继承关系,需要先抽象,再继承。
抽象即总结相似之处,总结对象之间的相似之处得到类,总结类与类之间的相似之处就可以得到父类。
class Teacher:
school = '无名大学'
def __init__(self, name, sex, age, level):
self.name = name
self.sex = sex
self.age = age
self.level = level
def info(self):
print('教师姓名:%s|教师性别:%s|教师年龄:%s|教师职级:%s' % (self.name, self.sex, self.age, self.level))
class Studet:
school = '无名大学'
def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age
def info(self):
print('学生姓名:%s|学生性别:%s|学生年龄:%s' % (self.name, self.sex, self.age))
上述代码中,我们定义了同一所大学的教师类与学生类,并各自有输出基本信息功能。
类Teacher与Student之间存在重复的代码,老师与学生都是人类,所以我们可以得出如下继承关系,实现代码重用:
class People:
school = '无名大学'
def __init__(self, name, sex, age):
self.name = name
self.sex = sex
self.age = age
class Teacher(People):
def __init__(self, name, sex, age, level):
# 指名道姓地跟父类People去要__init__
People.__init__(self, name, sex, age)
self.level = level
def info(self):
print('教师姓名:%s|教师性别:%s|教师年龄:%s|教师职级:%s' % (self.name, self.sex, self.age, self.level))
class Studet(People):
def info(self):
print('学生姓名:%s|学生性别:%s|学生年龄:%s' % (self.name, self.sex, self.age))
Student类内并没有定义__init__
方法,但是会从父类中找到__init__
,因而仍然可以正常实例化。
Teacher类内比父类多了一个self.level
属性,但其他三个属性是符合父类的。所以在Teacher类内还需要定义一个__init__
方法,这个方法不仅先要指名道姓的去父类内要父类的__init__
方法(相当于调用父类内的__init__
方法),还要补充上父类没有的self.level
属性。
stu_obj = Studet('山炮', '男', 18)
tea_obj = Teacher('李雷', '男', 29, '二级教师')
print(stu_obj.__dict__) # {'name': '山炮', 'sex': '男', 'age': 18}
print(tea_obj.__dict__) # {'name': '李雷', 'sex': '男', 'age': 29, 'level': '二级教师'}
stu_obj.info() # 学生姓名:山炮|学生性别:男|学生年龄:18
tea_obj.info() # 教师姓名:李雷|教师性别:男|教师年龄:29|教师职级:二级教师
三、单继承下的属性查找
有了继承关系,对象在查找属性时,先从对象自己的__dict__
中找,如果没有则去实例化自己的子类中找,然后再去父类中找……
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()
# 运行结果:
# Foo.f2
# Foo.f1
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()
# 运行结果:
# Foo.f2
# Foo.f1
四、多继承下的菱形问题与MRO
(一)菱形问题
大多数面向对象语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的。
这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发菱形问题
(或称钻石问题
),菱形其实就是对下面这种继承结构的形象比喻。
这种继承结构下导致的问题称之为菱形问题
:即如果A
(一定是非object类
)中有一个方法,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的继承实现原理(MRO)
。
(二)MRO
Python到底是如何实现继承的呢?
对于我们定义的每一个类(不管有没有继承),Python都会计算出一个方法解析顺序(MRO)列表
,该MRO列表
就是一个简单的所有基类的线性顺序列表。
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
print(D.mro())
# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
类相关的属性查找(类名.属性
、该类的对象.属性
),都是参照该类的mro列表顺序
进行查找的。
比如类D
以及类D的对象
访问属性都是参照类D的mro列表
,类C
以及类C的对象
访问属性都是参照类C的mro列表
。
五、广度优先(不论版本)与深度优先(只存在于python2)
如果多继承是非菱形继承,继承结构如下图所示:
此时,会按照先找B
这一条分支,然后再找C
这一条分支,最后找D
这一条分支的顺序进行查找,直到找到我们想要的属性。即一个分支一个分支地找到底,所有分支找完了最后找object(经典类与新式的属性查找顺序一样)。
与上图相对应的代码如下:
class E:
def test(self):
print('from E')
class F:
def test(self):
print('from F')
class B(E):
def test(self):
print('from B')
class C(F):
def test(self):
print('from C')
class D:
def test(self):
print('from D')
class A(B, C, D):
pass
print(A.mro())
# [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>]
如果继承关系为菱形结构,继承结构如下图所示:
此时,会按照先找B这一条分支找到E停下不去找G
,然后再找C这一条分支找到F停下不去找G
,然后找D再去找G
,最后找object
的顺序进行查找,直到找到我们想要的属性。
与上图相对应的代码如下:
class G():
def test(self):
print('from G')
class E(G):
pass
class F(G):
def test(self):
print('from F')
class B(E):
pass
class C(F):
def test(self):
print('from C')
class D(G):
def test(self):
print('from D')
class A(B, C, D):
pass
obj = A()
obj.test() # from C
print(A.mro())
# [<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->B->E->C->F->D->G->object
这种菱形结构下“查找不找底,最后才找底”
的方式我们称为广度优先。
有一种特殊情况,即在python2版本中,当多继承菱形结构的顶级父类是经典类
时,查找顺序遵循的是深度优先。
如下图所示:
这种“第一次就找到底,随后几次不找底”
的方式我们称为深度优先。(只存在于python2中)
总结: 如果多继承是菱形继承,经典类与新式类的属性查找顺序不一样:
- 经典类:深度优先,会在检索第一条分支的时候就直接一条道走到黑,即会检索大脑袋(共同的父类)
- 新式类:广度优先,会在检索最后一条分支的时候检索大脑袋
经典类: 在python2中,未继承object的类及其子类,都是经典类。
新式类: 在python3中,所有的类都是新式类;在python2中,继承了object的类及其子类才是新式类。
'''
多继承到底要不用???
要用,但是规避几点问题
1、继承结构尽量不要过于复杂
2、推荐使用mixins机制
'''
六、多继承的正确打开方式:mixins规范
mixins规范核心:就是在多继承背景下尽可能地提升多继承的可读性
多继承是Python语言独有的特性, 一个子类可以同时继承多个父类。
我们拿食物来举一个例子:麻辣鸡丝、可口可乐、雪碧都属于是食物,后两者都有一个特性或者功能是味道甜,但是麻辣鸡丝是辣的,所以如下所示,我们把“甜”放到食物这个顶级父类中是极不合理的,容易引起歧义。
class food: # 食物
def 甜的(self):
print("I am sweet")
class 雪碧(food):
pass
class 可口可乐(food):
pass
class 麻辣鸡丝(food): # 麻辣鸡丝并不是甜的,是辣的,但按照这个继承关系,麻辣鸡丝也是甜的
pass
但是如果可乐和雪碧都各自写自己的“甜的”方法,又违背了代码尽可能重用的原则(如果以后甜味的食物越来越多,那会重复代码将会越来越多)。怎么办?
为了尽可能地重用代码,那就只好再定义出一个“甜味”的类,然后让可乐和雪碧同时继承食物以及“甜味”两个父类,这样就出现了多重继承。当代码写的越来越多时,这种多重继承也将越来越多,父类与子类的从属关系将编织成一张大网,眼花缭乱。
所以Python提供了Mixins机制,简单来说Mixins机制指的是给子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(MixIn后缀),以此标识这些类只是用来混合功能的,并不是用来标识类间的从属关系(我虽然是继承了他的属性和功能,但我可不是他儿子啊,我俩没有血缘[从属]关系)。所以Mixins机制本质仍是多继承,但却是一种很好地解决上述问题的规范。
class food: # 食物
pass
class SweetMixIn:
def 甜的(self):
print("I am sweet")
class 雪碧(SweetMixIn, food):
pass
class 可口可乐(SweetMixIn, food):
pass
class 麻辣鸡丝(food):
pass
使用Mixin类实现多重继承要非常小心
- 首先它必须表示某一种功能,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
- 其次它必须责任单一,一个类可以继承多个Mixin
- 然后,它不依赖于子类的实现
- 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如可乐照样可以吃(喝),就是不甜了)
Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向。
七、在子类派生的新方法中如何重用父类的功能
方式一:指名道姓调用某一个类下的函数 ==》不依赖
于继承关系
class People:
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:
def __init__(self, name, age, sex, level):
People.__init__(self, name, age, sex)
self.level = level
tea_obj = Teacher('张三', 28, '男', '二级教师')
print(tea_obj.__dict__) # {'name': '张三', 'age': 28, 'sex': '男', 'level': '二级教师'}
方式二:super()调用父类提供给自己的方法 ==》严格依赖
继承关系
class People:
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(People):
def __init__(self, name, age, sex, level):
super().__init__(name, age, sex) # 调用的是方法,自动传入对象
self.level = level
tea_obj = Teacher('张三', 28, '男', '二级教师')
print(tea_obj.__dict__) # {'name': '张三', 'age': 28, 'sex': '男', 'level': '二级教师'}
调用super()
会得到一个特殊的对象,该对象会参照发起属性查找的那个类
的mro
,按照mro
的顺序寻找属性。
super()案例
class A:
def test(self):
print('from A')
super().test()
class B:
def test(self):
print('from B')
class C(A, B):
pass
obj = C()
obj.test()
# 运行结果:
# from A
# from B
print(C.mro()) # [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
在上例中,发起属性查找
的那个类是C
,不是A
;所以查找顺序是按照C的mro
进行查找。