继承
通过前面的案例,我们了解了面向对象的第一个特性——封装,即将属性和方法根据职责封装到一个抽象的类中
本篇将为大家介绍面向对象的第二大特性——继承。继承实现代码的复用,相同的代码不需要重复的编写
1. 单继承
1.1 继承的概念、语法和特点
现在我们需要定义四个类,分别是Animal、Dog、XiaoTianQuan以及Cat,如果在不使用继承方法,只用我们前面的方法,就是各个类封装对应的属性和方法,假设他们的属性方法如下所示:
首先定义一个简单动物类,包括吃、喝、跑和睡等方法。然后我们用该类来创建一个狗对象。但有一个问题、就是狗还有一个bark方法,如果我们想要实现该方法,就要在定义一个Dog类,然后在上一个类的四个方法的基础上在加上一个方法。具体如下:
class Animal:
def eat(self):
print('吃')
def drink(self):
print('喝')
def run(self):
print('跑')
def sleep(self):
print('睡')
class Dog:
def eat(self):
print('吃')
def drink(self):
print('喝')
def run(self):
print('跑')
def sleep(self):
print('睡')
def bark(self):
print('汪汪汪~')
# 创建狗对象旺财
wangcai = Animal()
wangcai.eat()
wangcai.drink()
wangcai.run()
wangcai.sleep()
在上述实现的代码中,我们可以发现一些明显的问题,我们对的动物类和狗类有很多一样的方法,这样重复写属实费劲,且创建的动物对象旺财还不能调用bark方法。虽然我们用狗类来创建对象是可以调用该方法的,那我们完全就不需要动物类,这样说来,定义一个动物类不就是多此一举?我们往后看,如果将哮天犬看为一个单独品种的狗,那么为了实现哮天犬的fly方法,是不是还需要在创建一个封装了吃、喝、睡、跑、叫等再加一个fly方法的哮天犬类,这样重复的工作就会很多,不符合我们使用类来高效开发的初衷。
为了减少在开发过程中的重复工作,我们使用继承的方法实现各种需求,我们以动物类为例,若要定义狗类,不需要重写那些重复的方法,只需要将原来的动物类试做一个基类,在定义狗类的时候继承该类的属性和方法,这样我们如果有新的属性和方法,就在新创建的类中编写即可。上图所示的类可以简化如下所示:
首先,狗和猫都是属于动物的,所以在动物类封装了基本的属性和方法,狗和猫类只需要通过继承即可获得动物类的所有属性和方法,这个过程中,动物类是父类,猫类和狗磊都是动物类的子类,狗类在父类基础上增加一个bark方法,而哮天犬又通过继承获得狗类的全部属性和方法,
1.2 继承语法:
calss 类名(父类名):
pass
- 子类继承自父类,可以直接享受分类中已经封装好的方法,不需要再次开发
- 子类中应该根据职责封装自身特有的属性和方法。
- 在上的继承关系中,狗类是动物类的派生类,动物类是狗类的基类,狗类从动物类派生
class Animal:
def eat(self):
print('吃')
def drink(self):
print('喝')
def run(self):
print('跑')
def sleep(self):
print('睡')
class Dog(Animal):
def bark(self):
print('汪汪汪~')
# 创建狗对象旺财
wangcai = Dog()
wangcai.eat()
wangcai.drink()
wangcai.run()
wangcai.sleep()
wangcai.bark()
结果如下:
吃
喝
跑
睡
汪汪汪~
1.3 继承的传递性
C类从B类继承,B类又从A类继承,那么C类就具有B类和A类的所有属性和方法。子类拥有父类以及父类的父类中封装的所有属性和方法。例如:
class Animal:
def eat(self):
print('吃')
def drink(self):
print('喝')
def run(self):
print('跑')
def sleep(self):
print('睡')
class Dog(Animal):
def bark(self):
print('汪汪汪')
class XiaoTianQuan(Dog):
def fly(self):
print('飞')
# 创建一个哮天犬对象
xtq = XiaoTianQuan()
xtq.run()
xtq.bark()
xtq.fly()
结果为:
跑
汪汪汪
飞
我们首先定义了一个动物类,然后定义一个狗类,狗类继承自动物类,然后又定义了一个哮天犬类继承自狗类,结果也证明了,哮天犬类创建的实例对象,不仅可以调用自己封装的方法fly(),还能够调用父类的方法bark(),也能够调用父类的父类的方法run()。
2.方法重写
现在我们知道了继承自父类的子类拥有父类的所有属性和方法,当父类的方法实现不能满足子类需求时,可以对方法进行 重写(override) 。父类重写有两种情况:
- 覆盖父类的方法
- 对父类的方法进行扩展。
2.1 覆盖方法
如果在开发过程中,父类的方法实现和子类的方法实现完全不同。可以使用覆盖的方式在子类中重新编写父类的方法实现,具体的实现方式就是在子类中定义一个和父类同名的方法并且实现。重写之后,在运行时,只会调用子类中重写的方法,而不再会调用父类封装的方法。
我们现在对哮天犬类的部分方法进行重写:
class Animal:
def eat(self):
print('吃')
def drink(self):
print('喝')
def run(self):
print('跑')
def sleep(self):
print('睡')
class Dog(Animal):
def bark(self):
print('汪汪汪')
class XiaoTianQuan(Dog):
def fly(self):
print('飞')
# 重写父类的bark方法
def bark(self):
print("嗷呜~~~")
# 重写父类的父类的方法
def run(self):
print("像神一样奔跑")
xtq = XiaoTianQuan()
xtq.bark()
xtq.run()
xtq.fly()
在上述过程中,我们对哮天犬的父类中封装的bark()方法以及父类的父类中的run()方法都进行重写,运行结果如下:
嗷呜~~~
像神一样奔跑
飞
可以看到,方法已经重写成功,程序只调用子类中重写的方法,不在调用父类和父类的父类中封装的方法。
2.2 扩展方法
如果在开发过程中,父类的方法不能够满足需求时,就需要对父类的方法进行扩展来满足开发需求,而父类原本封装的方法实现是子类的一部分,子类若是需要分类的方法,就需要用到super().的方式来调用父类方法。
class Animal:
def eat(self):
print('吃')
def drink(self):
print('喝')
def run(self):
print('跑')
def sleep(self):
print('睡')
class Dog(Animal):
def bark(self):
print('汪汪汪')
class XiaoTianQuan(Dog):
def fly(self):
print('飞')
# 重写父类的bark方法
def bark(self):
# 针对子类特有的需求编写功能
print('嗷呜~~~')
# 使用super().方法调用原本分类中封装好的方法
super().bark()
# 增加其他功能
print('你在狗叫什么~~~')
xtq = XiaoTianQuan()
xtq.bark()
xtq.run()
xtq.fly()
在上述方法中,哮天犬类的bark()方法除了自身扩展的方法外,还能够使用父类的方法,结果如下:
嗷呜~~~
汪汪汪
你在狗叫什么~~~
跑
飞
在调用父类的方法时,还可以用父类名.方法(self),将哮天犬类中该部分进行修改如下:
...
class XiaoTianQuan(Dog):
def fly(self):
print('飞')
# 重写父类的bark方法
def bark(self):
# 针对子类特有的需求编写功能
print('嗷呜~~~')
# 使用super().方法调用原本分类中封装好的方法
# super().bark()
Dog.bark(self)
# 增加其他功能
print('你在狗叫什么~~~')
这个方法不推荐使用,因为一旦父类发生变化,方法调用为自对应的父类名同样需要修改。有个需要注意的点就是使用子类调用自身方法时会出现递归调用,死循环报错。
3.父类的私有属性和方法
前面我们已经了解到了类的外部不能访问私有属性和方法,那现在我们继承自父类的子类中能不能访问父类中的私有属性和方法呢?我们可以实践一下。不过我们首先要明白的一点是,既然类的外部不能访问私有属性和方法,现在一个继承自A类的子类B的外部肯定不能从外部通过B类去访问A类的私有属性和方法。所以我们只需要测试一下B类内部能不能访问A类的私有属性和方法即可:
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200
def __test(self):
print(f'私有属性{self.num1}、{self.__num2}')
class B(A):
def demo(self):
# 访问A类私有属性
print(self.num1) # 访问公有属性
# print(self.num2) # 1.访问私有属性
# self.__test() # 2.调用私有方法
# 创建一个子类对象
b = B()
b.demo()
测试结果如下:
1.访问私有属性
100
Traceback (most recent call last):
File "D:\python study\python面向对象练习\pythonProject\面向对象进阶\06.py", line 19, in <module>
b.demo()
File "D:\python study\python面向对象练习\pythonProject\面向对象进阶\06.py", line 14, in demo
print(self.num2) # 1.访问私有属性
AttributeError: 'B' object has no attribute 'num2'. Did you mean: 'num1'?
2.调用私有方法
100
Traceback (most recent call last):
File "D:\python study\python面向对象练习\pythonProject\面向对象进阶\06.py", line 19, in <module>
b.demo()
File "D:\python study\python面向对象练习\pythonProject\面向对象进阶\06.py", line 15, in demo
self.__test() # 2.调用私有方法
AttributeError: 'B' object has no attribute '_B__test'. Did you mean: '_A__test'?
从结果我们可以看到,无论是私有属性或是私有方法,都是不可以访问的。
现在我们通过实践已经知道了子类无法直接访问父类的私有方法和属性,那有没有一种能够让子类访问到父类的私有属性和方法呢?既然有需求,那肯定是有方法能够去实现的,不过过程需要一点转弯。我们可以思考一下,既然子类能够访问父类的公有属性和方法,而父类的内部又可以访问私有属性和方法,那我们是不是可以在父类的内部设计一个可以访问私有属性和方法的公有方法,然后子类再去通过继承的特点调用该方法,这样岂不是可以从外部访问分类的私有属性和方法了吗?理论上是没有问题的,我们直接实践检验一下即可。
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200
def __test(self):
print(f'私有属性{self.num1}、{self.__num2}')
def test(self):
self.__test()
class B(A):
def demo(self):
# 访问A类私有属性
print(self.num1) # 访问公有属性
# print(self.num2) # 1.访问私有属性
# self.__test() # 2.调用私有方法
self.test()
# 创建一个子类对象
b = B()
b.demo()
结果为:
100
私有属性100、200
通过一个中间方法,我们就可以从外部一点一点向类的内部进行过渡,从而间接访问到父类的私有属性和方法。
4.多继承
我们前面学到的单继承是子类可以继承父类所有属性和方法,并且可以对父类的属性和方法进行重写。而多继承就是一个子类可以拥有多个父类,并且具有所有父类的属性和方法。例如,孩子会继承自己母亲和父亲的特性
语法:
class 子类名(父类名1,父类名2...):
pass
我们定义几个简单的类来实践一下
class A:
def test(self):
print("test方法")
class B:
def demo(self):
print("demo方法")
class C(A, B):
pass
c = C()
c.test()
c.demo()
结果为:
test方法
demo方法
既然子类可以同时具有多个父类的属性和方法,如果不同的父类中存在同名的方法,子类在调用方法时,会调用哪一个父类中的方法呢。例如A类和B类当中都有test方法和demo方法,那么C类再调用方法时会调用哪个类中的方法?
class A:
def test(self):
print("A--test方法")
def demo(self):
print("A--demo方法")
class B:
def test(self):
print("B--test方法")
def demo(self):
print("B--demo方法")
class C(A, B):
pass
c = C()
c.test()
c.demo()
结果为:
A--test方法
A--demo方法
从结果中可以看出,C类调用的是A类中的方法。实际上,当一个类有多个父类的时候,默认使用第一个父类的同名属性和方法。不过在开发过程中,应该尽量避免这种容易产生混淆的情况,父类之间尽量避免出现相同属性名或方法名。
而针对多继承时默认继承第一个类的同名属性和方法,python中有一个内置属性__mro__可以查看方法的顺序搜索。MRO即method resolution order,主要用于多继承时判断方法、属性的调用路径。对于上述A、B、C是三个类,我们可以使用
print(C.__mro__) # 输出为元组型
print(C.mro()) # 输出为列表型
来查看调用方法的顺序。结果为
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
在搜索方法时,是按照输出结果从左往右的顺序查找的,如果在当前类未找到方法就会直接执行,不在搜索。如果没有找到,就查找下一个类中是否有对应的方法,直至最后一个类,若是都没有找到,程序变回报错。
5.object
在上一节中,我们查看子类调用方法顺序时看到结果当中输出一个object,那么这个object是什么意思呢?
其实,object是python为所有对象提供的一个基类,所有其他的类都直接或间接地继承自它。它为所有类提供了一些基本的方法和属性,并定义了类的行为和实例的创建过程。