[Python学习日记-63] 继承与派生
简介
继承
派生
简介
上一篇文章我们学习了类如何使用,以及相关特性,也做了相关的练习,在练习当中发现类与类之间有时也会存在重复代码,其实在类中我们还有一个继承和派生的概念没有说,下面我们一起来看看到底是怎么一回事吧。
继承
一、初识继承
继承指的是类与类之间的关系,是一种什么“是”什么的关系,继承的功能之一就是用来解决代码重用问题继承是一种创建新类的方式,在 Python 中,新建的类可以继承一个或多个父类,父类又可以称为基类或超类,新建的类称为派生类或子类。Python 中类的继承分为单继承和多继承,如下
class ParentClass1: # 定义父类
pass
class ParentClass2: # 定义父类
pass
class SubClass1(ParentClass1): # 单继承,基类是 ParentClass1,派生类是 SubClass
pass
class SubClass2(ParentClass1,ParentClass2): # Python 支持多继承,用逗号分隔开多个继承的类
pass
# 查看当前类的父类
print(SubClass1.__bases__) # __base__ 只查看从左到右继承的第一个子类,__bases__ 则是查看所有继承的父类
print(SubClass2.__bases__)
代码输出如下:
在 Python 中存在两种类,分别是经典类与新式类,具体的区别将会在后面的原理解析中进行介绍,我们先看看这两个类的一个特征:
- 只有在 Python2 中才分新式类和经典类,Python3 中统一都是新式类
- 在 Python2 中,没有显式的继承 object 类的类,以及该类的子类,都是经典类
- 在 Python2 中,显式的声明继承 object 类的类,以及该类的子类,都是新式类
- 在 Python3 中,无论是否继承 object,都默认继承 object,即 Python3 中所有类均为新式类
注意:如果没有指定基类,Python 的类会默认继承 object 类,object 是所有 Python 类的基类,它提供了一些常见方法(如 __str__)的实现。
print(ParentClass1.__bases__)
print(ParentClass2.__bases__)
代码输出如下:
二、继承与抽象(先抽象再继承)
抽象即抽取类似或者说比较像的部分。其中抽象分成两个层次:
- 将奥巴马和梅西这两个对象比较像的部分抽取成类
- 将人、猪、狗这三个类比较像的部分抽取成父类
抽象:最主要的作用是划分类别(可以隔离关注点,降低复杂度),如下图所示
继承:是基于抽象的结果,通过编程语言去实现它,肯定是先经历抽象这个过程,才能通过继承的方式去表达出抽象的结构。
其实抽象只是分析和设计的过程中的一个动作或者说一种技巧,开发时我们可以通过抽象可以得到类。继承的关系如下图所示
三、继承与重用性
在上一篇文章中,我们有一个练习是模仿 LOL 开发了一个英雄相互攻击的程序,分析一下练习答案的代码发现类 Garen 和类 Riven 都是英雄,而且属性也有一部分是相同的,如果我们建立了一个类 Hero,类 Hero的大部分内容与类 Garen 和类 Riven 的相同时,我们写的类 Garen 和类 Riven 的大多数代码都显得非常的多余,所以这就要用到了类的继承的概念了。通过继承的方式新建类 Garen 和类 Riven,让类 Garen 和类 Riven 继承类 Hero,类 Garen 和类 Riven 会“遗传”类 Hero 的所有属性(数据属性和函数属性),实现代码重用,如下
class Hero:
def __init__(self, nickname, life_value, aggresivity):
self.nickname = nickname
self.life_value = life_value
self.aggresivity = aggresivity
def attack(self, enemy):
enemy.life_value -= self.aggresivity
class Garen(Hero):
pass
class Riven(Hero):
pass
g1 = Garen('草丛轮', 100, 30)
r1 = Riven('可爱的瑞文', 80, 50)
print(g1.life_value)
r1.attack(g1)
print(g1.life_value)
代码输出如下:
注意:用已经有的类建立一个新的类,这样就重用了已经有的软件中的一大部分设置,大大节省了编程的工作量,这就是常说的软件重用,不仅可以重用自己的类,也可以继承别人的,例如标准库,来定制新的数据类型,这样就是大大缩短了软件开发周期,对大型软件开发来说,意义重大
四、继承时的属性查找
在之前我们介绍类与对象的时候已经介绍过类的属性查找了,当时是说当通过对象来访问属性时,会先在对象自己的命名空间里查找,然后才到类里面查找,而现在再加入继承的情况下,那查找顺序又是怎么样的呢?我们来看看下面的代码
class Foo: # 3:之后再找父类的命名空间
def f1(self):
print('from Foo.f1')
def f2(self):
print('from Foo.f2')
self.f1() # b.f1()
class Bar(Foo): # 2:再找类的命名空间
def f1(self):
print('from Bar.f1')
b = Bar()
b.f2() # 1:先找 b 自己的命名空间
代码输出如下:
在有继承的情况下, 会先查找对象自己的命名空间,然后再去查找自己的类的命名空间,最后才会去查找父类的命名空间。上述的都是继承单个父类的情况,我们知道 Python 是支持继承多个父类的,而这种情况会比较复杂,详细的我们将在继承的实现原理里面介绍。
五、继承的实现原理
在之前使用类的继承的时候我们都只是使用了单个父类的继承,并没有使用过多个父类的继承,在涉及到多个父类的继承时,如果多个父类中有重名的属性那我们应该按照什么顺序来进行查找呢?那我们要先了解一下 Python 是如何实现继承的。
Python 到底是如何实现继承的,对于你定义的每一个类,Python 会计算出一个方法解析顺序(MRO)列表,这个 MRO 列表就是一个简单的所有基类的线性顺序列表,代码如下
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'>]
为了实现继承 Python 会在 MRO 列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。而这个 MRO 列表的构造是通过一个 C3 线性化算法来实现的。我们不去深究这个算法的数学原理(有兴趣的小伙伴可以自己去查查),它实际上就是合并所有父类的 MRO 列表并遵循如下三条准则:
- 子类会先于父类被检查
- 多个父类会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,则选择第一个父类
在其他语言中,例如 Java 和 C# 中子类只能继承一个父类,而 Python 中子类可以同时继承多个父类,如果继承了多个父类,那么属性的查找方式有两种,分别是深度优先和广度优先,如下:
当类是经典类时,多继承情况下,在要查找属性不存在时,会按照深度优先的方式查找下去
当类是新式类时,多继承情况下,在要查找属性不存在时,会按照广度优先的方式查找下去
示例代码:
# 在 Python3 中也可以写成 class G(object):
# 在 Python2 中
# 经典类:class G:(没有继承 object 类)
# 新式类:class G(object):
class G:
def test(self):
print('from G')
class F(G):
def test(self):
print('from F')
class E(G):
def test(self):
print('from E')
class D(G):
def test(self):
print('from D')
class C(F):
def test(self):
print('from C')
class B(E):
def test(self):
print('from B')
class A(B,C,D):
def test(self):
print('from A')
a1 = A()
a1.test()
print(A.mro(),'\n',A.__mro__) # 只有新式才有这个属性可以查看线性列表,经典类没有这个属性
- 新式类继承顺序:A -> B -> E -> C -> F -> D -> G
- 经典类继承顺序:A -> B -> E -> G -> C -> F -> D
注意:可以尝试一下注释掉不同类当中的 test 函数来观察以下输出结果
派生
在继承了父类的同时,子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了,如下
class Hero:
def __init__(self, nickname, life_value, aggresivity):
self.nickname = nickname
self.life_value = life_value
self.aggresivity = aggresivity
def attack(self, enemy):
enemy.life_value -= self.aggresivity
class Garen(Hero):
camp = 'Demacia' # 派生的变量,子类独有
def attack(self, enemy): # 派生的方法会重写从父类当中继承的方法
enemy.life_value -= self.aggresivity
print('attack for Garen...')
class Riven(Hero):
camp = 'Noxus'
def attack(self, enemy): # 在自己这里定义新的 attack,不再使用父类的 attack,且不会影响父类
print('attack for Riven... ')
def fly(self): # 在自己这里定义新的
print('%s is flying' % self.nickname)
g = Garen('草丛轮',100,30)
r = Riven('瑞问问',80,50)
print(g.camp)
g.attack(r)
print(r.life_value)
r.fly()
代码输出如下:
在子类中有时候可能你并不是想完全重写父类中的方法,可能只是想在父类的基础上加上一些自己独有的属性或者参数,而 Python 提供了两种方法来解决这个问题:
1、调用普通函数的方式(指名道姓,不依赖继承)
class Garen(Hero):
camp = 'Demacia'
def __init__(self,nickname,life_value,aggresivity,weapon):
Hero.__init__(self,nickname,life_value,aggresivity)
self.weapon = weapon
def attack(self, enemy):
Hero.attack(self, enemy) # 和普通函数没什么两样
enemy.life_value -= self.aggresivity
print('attack for Garen...')
class Riven(Hero):
camp = 'Noxus'
def attack(self, enemy):
Hero.attack(self, enemy)
print('attack for Riven... ')
def fly(self):
print('%s is flying' % self.nickname)
g = Garen('草丛轮',100,30,'金箍棒')
r = Riven('瑞问问',80,50)
print(g.__dict__)
print(g.camp)
g.attack(r)
print(r.life_value)
r.fly()
代码输出如下:
这种方式并不依赖继承,所以它会像普通函数一样来进行调用,即类名.func(),此时就与调用普通函数无异了,因此即便是 self 参数也要为其传值。
2、super()(依赖继承)
class Garen(Hero):
camp = 'Demacia'
def __init__(self,nickname,life_value,aggresivity,weapon):
super(Garen, self).__init__(nickname,life_value,aggresivity) # Python2 当中一定要这样使用
self.weapon = weapon
def attack(self, enemy):
super().attack(enemy) # Python3 可以简写成 super()
enemy.life_value -= self.aggresivity
print('attack for Garen...')
class Riven(Hero):
camp = 'Noxus'
def attack(self, enemy):
Hero.attack(self, enemy) # 指名道姓的形式,不依赖继承
print('attack for Riven... ')
def fly(self):
print('%s is flying' % self.nickname)
g = Garen('草丛轮',100,30,'金箍棒')
r = Riven('瑞问问',80,50)
print(g.__dict__)
print(g.camp)
g.attack(r)
print(r.life_value)
r.fly()
代码输出如下:
这种方式调用父类的属性是依赖于继承的,为什么说是依赖于继承呢?这是因为当有多个父类的时候他的查找顺序是会按照介绍原理时提到的 MRO 列表的顺序来进行查找的,为了方便理解请看下面的代码
class A:
def f1(self): # c.f1() 执行后 MRO 列表查找到 A 的位置 super() 则接着往下找
print('from A')
super().f1() # 会根据 方法解析顺序(MRO)列表 来进行查询,所以就算没有定义父类也会按照 MRO 列表的顺序找到 B 中的 f1
class B:
def f1(self):
print('from B')
class C(A,B):
pass
print(C.__mro__) # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
c = C()
c.f1() # 基于 C 的 MRO 列表来查找
代码输出如下:
根据输出可以看到即使类 A 没有把类 B 设置为父类,super() 也会根据 MRO 列表的排列顺序进行查找。