此篇为Python面向对象的第二篇,主要讲述面向对象三大特征中的"继承",第一篇为封装篇,没有相关基础的读者可以移步Python——面向对象(OOP)封装篇
继承
就像子女会继承父辈的家产一样,这里的继承也是同一个意思,具体指,如果一个类A继承了另一个类B(或者另一些类),则类A就继承了类B(或者被继承的那些类)的所有类属性和方法,即类A可以使用类B(或者被继承的那些类)中的类属性和方法。根据继承的类是一个还是两个及两个以上的情况,分为单继承和多继承
继承可以使一个类能够使用被继承类的中的代码,即继承可以提高代码复用率,降低冗余性,提高开发效率
通俗来讲,通过继承,我们可以实现一个类中即使没有定义某一个方法,但是只有其父类中具有该方法,就可以实现正常的调用(调用的是父类中的那个方法),完成一定功能,这就是继承
当类对象与类对象之间存在相同的内容,并满足子类对应的事物为父类对应事物中的一种时,就可以考虑使用继承,来提高代码复用性
Python中的继承
Python是支持多继承的,而且Python2和Python3在继承这一块内容有区别,Python2中如果一个类没有对被继承的类进行指定(即下面代码这种情况),就是一个经典类(通俗来说就是比较老,默认没啥方法)
class C:
pass
Python2中如果一个类对被继承的类进行了指定(即下面代码这种情况),就是一个新式类(通俗来讲就是具有一些比较NB的默认方法)
class C(object):
pass
但是,在Python3中,一个类如果没有进行指定,默认继承了object类,即Python3中已经去除了经典类,全部都是新式类(既然全部都是新式类了,那也就没必要说啥新式不新式的了,统一都叫做类),都具有NB的默认方法
即下面这三种写法是等效的
class C:
pass
class C():
pass
class C(object):
pass
其实Python在使用类创建实例对象的时候,也是通过一个方法(名为__new__)完成的,即没有这个方法,实例对象根本就不会被创建出来,而之前我直接在一点自定义类对象中写上pass,也可以创建实例对象成功,这其实就是因为,该类继承了object,而object类对象中是具有该方法的,于是进行实例化对象的时候,就会继承object类对象中的该方法,实现对象实例化,这就是继承机制发挥作用的一个案例
class C:
pass
ins = C()
print(ins)
#输出结果:<__main__.C object at 0x000001EC9062AFD0>
父类
有时也称为基类,即被继承的类
子类
有时也称为派生类,即继承的类
比如类A继承了类B,则我们称类A为类B的子类(派生类),同时类B为类A的父类(基类)
单继承
仅仅继承了一个类,代码中体现为一个类的括号中仅仅写了一个类名
单继承的运作过程
- 对于一个类创建出来的实例对象,这个实例对象调用了实例方法,则解释器会先通过实例对象中的__class__属性去寻找该实例对象的类中是否具有该方法(前面说过实例对象中是不存储方法id的,方法id都在类对象中)
- 如果没有,则通过该类中的类属性去寻找该类的父类中是否具有该方法,如果没有,则继续去寻找其父类,…最后如果在最顶层的object类中依旧没有找到,则抛出异常
- 在这一整个寻找过程中,在任意一个环节寻找到了同名的方法,则对该方法进行调用,并不再继续寻找下去
class A:
def func(self):
print(self)
print('A的func实例函数')
class B(A):
def func(self):
print(self)
print('B的func实例函数')
class C(B):
pass
ins = C()
ins.func()
#输出结果:
"""
<__main__.C object at 0x00000275312FA6D0>
B的func实例函数
"""
多继承
继承了两个及两个以上的类,代码中体现为一个类的括号中写了多个类名,中间使用逗号隔开
多继承的运作过程
- 会按照该类括号中的父类的排列顺序,即从左到右逐个进行查找,针对每一个父类的查找可以参考单继承的查找方式,即先找到排列在第一个的父类中是否具有该方法,再寻找该父类的父类是否具有该方法…
- 如果第一个父类中都没有找到同名方法,则开始对第二个父类进行查找…如果从所有的父类那里都无法找到同名方法,则抛出异常
- 和单继承一样,在这一整个寻找过程中,在任意一个环节寻找找到了同名的方法,则对该方法进行调用,并不再继续寻找下去
class A:
def func(self):
print(self)
print('A的func实例函数')
class B:
def func(self):
print(self)
print('B的func实例函数')
class C(B,A):
pass
ins = C()
ins.func()
#输出结果:
"""
<__main__.C object at 0x000001A7DCDDAB20>
B的func实例函数
"""
方法重写
如果子类对于继承自父类的某一个属性或者方法不满意,可以进行修改,而且即便对方法进行了修改,对父类及其他子类是没有任何影响的,即父类和父类的所有子类还是可以调用父类的原来的方法,仅仅就是本类和本类的子类的同名方法被修改了而已
其实从上面继承机制的角度来看方法重写,就比较好理解了,这就是利用继承的查找机制实现的嘛,根本就没有什么新东西,子类的方法被重写,就是子类创建了一个同名的方法,当子类和子类的实例对象调用该方法的时候,就会执行继承的查找机制,当查找到该子类空间的时候,此时就会发现这个同名的方法,就会立即调用该方法,而且不会去父类中继续查找,而针对父类和其他没有进行该方法重写的子类来说,没有进行方法重写,在继承查找过程中就还是会去调用父类中定义的方法,即沿用父类中的代码,这就是所谓的方法重写的本质
class A:
def func(self):
print(self)
print('A的func实例函数')
class B(A):
def func(self):
print(self)
print('B重写后的func实例函数')
ins = B()
ins.func()
#输出结果:
"""
<__main__.B object at 0x00000219992AACD0>
B重写后的func实例函数
"""
我们经常对一个对象的__init__初始化方法进行方法重写
值得一提的是,其实Python中的运算符的计算就是通过调用类对象中的方法完成的,所以如果对这些与运算符对应的方法进行重写,就会导致运算机制发生改变,而这种通过重写方法来改变机制的过程,对于运算符来说,称为运算符重载
super方法
很多情况下,父类的方法并不是完全没有利用价值,即可能在方法重写以后,依旧要调用父类的方法进行一个补充,而此时子类就可以使用super方法去调用父类中定义的任意方法,即使在该子类中该方法被重写过了
class A:
def func(self):
print(self)
print('A的func实例函数')
class B(A):
def func(self):
print(self)
super().func() #使用super方法进行父类A的func实例对象的调用
print('B重写后的func实例函数')
ins = B()
ins.func()
#输出结果:
"""
<__main__.B object at 0x000001E7B544AB20>
<__main__.B object at 0x000001E7B544AB20>
A的func实例函数
B重写后的func实例函数
"""
我们在封装篇说过,一个实例方法是可以使用类对象进行调用的,只不过,此时实例方法不会自动传入实参(即一个实例对象的引用),如果需要使用实例对象的话,我们要手动传入一个实例对象
其实上面这个使用super的代码和下面这个不使用super的代码是等效的
class A:
def func(self):
print(self)
print('A的func实例函数')
class B(A):
def func(self):
print(self)
A.func(self) #通过父类A直接调用其实例方法,注意要手动传入一个实例对象的引用
print('B重写后的func实例函数')
ins = B()
ins.func()
#输出结果:
"""
<__main__.B object at 0x0000015B33DFACD0>
<__main__.B object at 0x0000015B33DFACD0>
A的func实例函数
B重写后的func实例函数
"""
这时就会有好奇的读者就会说了:这TMD不是吃饱了撑的没事干吗,我寻思使用super方法的时候,这代码量也没有少多少呀?(注:这绝不是作者初学super方法时的心理活动)
super方法的妙处其实要在下面这个案例中得以体现
class parent:
def func(self):
print('parent的func实例方法执行开始')
print('parent的func实例方法执行完毕')
class son1(parent):
def func(self):
print('son1重写后的func实例方法执行开始')
parent.func(self)
print('son1重写后的func实例方法执行完毕')
class son2(parent):
def func(self):
print('son2重写后的func实例方法执行开始')
parent.func(self)
print('son2重写后的func实例方法执行完毕')
class grandson(son1, son2):
def func(self):
print('grandson重写后的func实例方法执行开始')
son1.func(self)
son2.func(self)
print('grandson重写后的func实例方法执行完毕')
ins = grandson()
ins.func()
#输出结果:
"""
grandson重写后的func实例方法执行开始
son1重写后的func实例方法执行开始
parent的func实例方法执行开始
parent的func实例方法执行完毕
son1重写后的func实例方法执行完毕
son2重写后的func实例方法执行开始
parent的func实例方法执行开始
parent的func实例方法执行完毕
son2重写后的func实例方法执行完毕
grandson重写后的func实例方法执行完毕
"""
这个案例中的继承图如图所示,为一个钻石型的图案
明显地,无论是仅仅看着代码进行分析,还是直接看打印结果,都可以分析出这么一个调用过程:
明显,类对象parent中的func函数被调用了两次,想象一下这个过程中我们实际是要对一个数据进行一个初始化操作(在具体的项目中,确实很多时候就是要调用父类的方法进行属性的初始化操作),而且一般初始化要传入相应的参数的,比如看下面的简单的代码
class parent:
def __init__(self, name):
print('parent的__init__方法执行开始')
self.name = name
print('parent的__init__方法执行完毕')
class son(parent):
def __init__(self, name):
print('son重写后的__init__方法执行开始')
parent.__init__(self, name)
print('son重写后的__init__方法执行完毕')
ins = son('张三')
#输出结果:
"""
son重写后的__init__方法执行开始
parent的__init__方法执行开始
parent的__init__方法执行完毕
son重写后的__init__方法执行完毕
"""
如果将上面那个钻石型继承关系的代码中的所有实例方法__func__都改为上面这个传入参数的__init__方法,就可以想象,在grandson中调用父类son1的__init__的参数是完全没有起到任何作用的,全部都在son2的__init__方法调用后被覆盖掉了,即第一次对于父类son1的__init__的调用完全是无效的
再者,进行调用__init__方法进行实例对象的初始化的时候(一般在__init__方法中,都会调用父类的__init__方法),最终的那个类的__init__方法被调用了两次,这也是很奇怪的事情不是吗
而super方法的使用就可以解决这个问题(即最终的那个类的__init__方法被调用了两次,导致前一次调用的操作完全失效,是无用功的问题)
class parent:
def func(self):
print('parent的func实例方法执行开始')
print('parent的func实例方法执行完毕')
class son1(parent):
def func(self):
print('son1重写后的func实例方法执行开始')
super().func()
print('son1重写后的func实例方法执行完毕')
class son2(parent):
def func(self):
print('son2重写后的func实例方法执行开始')
super().func()
print('son2重写后的func实例方法执行完毕')
class grandson(son1, son2):
def func(self):
print('grandson重写后的func实例方法执行开始')
super().func()
print('grandson重写后的func实例方法执行完毕')
ins = grandson()
ins.func()
#输出结果:
"""
grandson重写后的func实例方法执行开始
son1重写后的func实例方法执行开始
son2重写后的func实例方法执行开始
parent的func实例方法执行开始
parent的func实例方法执行完毕
son2重写后的func实例方法执行完毕
son1重写后的func实例方法执行完毕
grandson重写后的func实例方法执行完毕
"""
我知道读者看到这个结果大概率非常迷惑,但是至少我们可以发现一点,从输出结果来看,最终的那个类的__init__方法只被调用了一次,非常符合我们的要求,即使用super方法貌似确实解决了我们上面的问题
下面讲解super方法运行的原理
-
super使用的算法
- 使用super方法调用父类方法的时候,实际上会依据一个算法(即C3算法)自行计算出应该要调用哪一个父类的方法(从上面的输出结果来看,在调用son1的过程中调用了son2,所以实际上不一定是本类的父类)
-
实际上,我们不需要去理解C3算法的运行过程,只需要通过打印使用super方法的类对象的__mro__类属性即可知道super方法的调用顺序
print(grandson.__mro__)
(<class '__main__.grandson'>, <class '__main__.son1'>, <class '__main__.son2'>, <class '__main__.parent'>, <class 'object'>)
我们可以看到,这是一个元组类型对象,其中,每一个元素都是一个类对象
并且顺序依次为grandson、son1、son2、parent、object(明显为广度优先的查找原则)
对照上面代码的输出结果,我们惊奇地发现,每一次调用super.__init__方法,都是去调用该元组中的位于后面的类对象中的同名方法
实际上,类对象的__mro__类属性为一个元组(该元组也称为"作用链"),所有的元素都是类对象,super方法被调用的时候,会选择本类对象所在位置的后一个具有同名方法的类对象中的那个同名方法进行调用,如果执行代码的过程中,又遇到了一个super方法(无论和第一个super方法调用的方法名是否相同),则在原来的那个作用链中继续选择下一个类的同名方法进行调用,即后续所有的被嵌套调用的super方法,要选择调用哪个类对象的方法的时候,都要去查看调用第一个super方法的类对象的__mro__类属性中各个类对象的排列顺序进行调用
接着我们康康super和__mro__的官方定义
-
super被调用后,会返回一个对象,该对象是类的父类或兄弟类。super对于调用类中已被重写的父类方法非常用
-
类的__mro__属性列出了getattr()和super()使用的搜索顺序。__mro__属性是动态变化的,可以在继承层次结构更新时进行内容的变化
简单来说就是super方法的本质是使用类对象调用其中的同名方法,但是这个具体是哪一个类对象,要看__mro__属性,__mro__属性就是管这个事的,而且__mro__属性是随着继承层次结构的变化而动态变化的
__mro__属性的动态变化
其实super方法的返回的并不一定是__mro__属性中本类的下一个类对象,严谨来说应该是下一个具有同名属性的类对象
比如将上面那个代码中类对象son2的func函数的代码删除后的运行结果
class parent:
def func(self):
print('parent的func实例方法执行开始')
print('parent的func实例方法执行完毕')
class son1(parent):
def func(self):
print('son1重写后的func实例方法执行开始')
super().func()
print('son1重写后的func实例方法执行完毕')
class son2(parent):
pass
class grandson(son1, son2):
def func(self):
print('grandson重写后的func实例方法执行开始')
super().func()
print('grandson重写后的func实例方法执行完毕')
ins = grandson()
print(grandson.__mro__)
ins.func()
#输出结果:
"""
(<class '__main__.grandson'>, <class '__main__.son1'>, <class '__main__.son2'>, <class '__main__.parent'>, <class 'object'>)
grandson重写后的func实例方法执行开始
son1重写后的func实例方法执行开始
parent的func实例方法执行开始
parent的func实例方法执行完毕
son1重写后的func实例方法执行完毕
grandson重写后的func实例方法执行完毕
"""
可以看到,grandson.__mro__的内容不变,但是在son1使用super方法的时候,并不是去调用son2的同名方法,而且son2中压根就没有这个名字的方法,于是son1会直接调用parent的同名方法
综上所述,super方法解决了在钻石型继承中父类方法最终会被调用多次,导致数据可能会被覆盖的问题
但是super方法的缺点在于,在多继承且为"钻石形继承"的情况下,写代码的时候,不知道最后在代码运行的时候,super方法使用的是哪一个类的方法(即代码的可读性差);而且不同类的方法,形参数量极有可能不同,所以一般都会在调用super方法的时候全部写上不定长参数(即*args 与**kwargs)(进一步减低了代码的可读性)
当然,在单继承的情况下,super方法的使用不会有问题,而且可以明确知道super方法使用的是哪一个类的方法