Python学习笔记(13)-黑马教程面向对象之继承

前言

这是黑马Python培训教程面向对象这一章中的继承(inheritance)部分。

什么是继承(inheritance)

观向对象的三大特征

  • 封装:根据需求将属性和方法封装到一个抽象的类中;
  • 继承:实现代码的重用,也就是说相同部分的代码不需要重复编写;
  • 多态:不同的对象调用相同的方法,产生不同的执行结果,增加代码的灵活度。

为什么要有继承

现在我们假设我们已经有了一个类,并且还要创建一个与之很像的类(可能只是新增了几个方法),该如何办呢(不能复制旧类的代码),解决思路如下:

例如我们有了一个名为Shape的类,它知道将自己绘制到屏幕上,现在我们想创建一个名为Rectangle的类,但它不仅知道如何将自己绘制到屏幕上,而且还知道如何计算其面积,我们不想重新编写方法draw,因此Shape已经了这样一个方法,且效果很好,那么我们就可以让Rectange继承Shape的方法,使得对Rectangle对象调用方法draw时,将自动调用Shape类的方法。

继承概念:子类拥有父母的所有方法和属性。

现在我们先看一下继承的一个优点,也就是前面提到的相同部分的代码不需要重复编写,现在先看一个简单的案例,在这个案例中,我们先不用继承这个特点来实现这一功能。

通过案例说明为什么需要继承

明确需求:假如我们在一个需求中,我们要定义动物类(Animal)与狗类(Dog),而动物类中则有4个方法,分别是吃(eat)、喝(drink),跑(run)和睡(sleep),而狗类(Dog)除了动物类(Animal)中的4个方法外,还有一个独特的方法,就是叫(bark),如下所示:

按照前面的原始方法(也就是不使用继承的思路),我们先定义一个动物类(Animal),然后使用这个动物类(Animail)创建一个叫“旺财”的狗对象,输入以下代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Animal:
def eat(self):
print("吃")
def drink(self):
print("喝")
def run(self):
print("跑")
def sleep(self):
print("睡")
# 创建一个叫“旺财”的狗对象,如下所示:
wangcai = Animal()
wangcai.eat()
wangcai.drink()
wangcai.run()
wangcai.sleep()
class Animal:
def eat(self):
print("吃")
def drink(self):
print("喝")
def run(self):
print("跑")
def sleep(self):
print("睡")
# 创建一个叫“旺财”的狗对象,如下所示:
wangcai = Animal()
wangcai.eat()
wangcai.drink()
wangcai.run()
wangcai.sleep()

结果运行如下所示:

1
2
3
4

假设,我们要在同一个代码中再定义一个狗类(Dog),前面提到,狗类(Dog)与动物类(Animal)的区别就在于狗类(Dog)多了一个叫(bark)的方法,定义完后,我们再通过个狗类(Dog)生成一个叫旺财(wangcai)的对象,此时我们定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Dog:
def eat(self):
print("吃")
def drink(self):
print("喝")
def run(self):
print("跑")
def sleep(self):
print("睡")
def bark(self):
print("汪汪叫")
# 创建一个叫“旺财”的狗对象,如下所示:
wangcai = Dog()
wangcai.eat()
wangcai.drink()
wangcai.run()
wangcai.sleep()
wangcai.bark()

运行结果如下所示:

1
2
3
4
5
汪汪叫

现在我们回顾一下动物类(Animal)与狗类(Dog)的代码,我们就可以发现,狗类(Dog)代码的前4个方法是直接从动物类(Animal)中复制过来的,如下所示:

如果在一个项目中,我们就这样定义了2个类,即动物类(Animal)与狗类(Dog),并且狗类(Dog)中的一部分与动物类(Animal)相同,我们可以把相同的代码复制过来,但是,如果还有其他的类,例如我们再定义一个哮天犬类(XiaoTianQuan),它比狗类(Dog)又多了一个方法,即飞(fly),如下所示:

1
2
3
4
5
6
7
XiaoTianQuan
eat(self):
drink(self):
run(self):
sleep(self):
bark(self):
fly(self):

那么,如果我们不使用继承的思路,还要复制相同部分的代码。此时,如果我们要修改eat(self)这个方法,就需要三次,即动物类(Animal)、狗类(Dog)、哮天犬类(XiaoTianQuan)都要修改,因此效率非常低下,代码量还多,容易出错。

为了解决这类问题(也就是A类,B类,C类中有相同的部分,并且C类由B类衍生而来,B类由A类衍生而来),因此面向对象中就有了继承这种思路。

继承的概念、语法和特点

类继承的概念:子类拥有父类的所有方法和属性。

前面我们提到了,我们定义了3个类,分别是动物类(Animal)、狗类(Dog)、哮天犬类(XiaoTianQuan)。在动物类(Animal)中有四个方法,即吃(eat)、喝(drink)、跑(run)、睡(sleep)这4个方法,这4种方法都是动物的基本特征。而狗(Dog)类可以看作是动物(Animal)的一个子集,也就是说狗类必然也有这4个方法,因此我们不需要重复编写这部分代码,可以从动物类中继承过来。

这里我们就可以说动物类(Animal)是父类,狗类(Dog)就是子类。

  • 子类继承自父类,可以直接享受父类中已经封装好的方法,不需要再次编写相同的代码;
  • 子类中应根据需求,封专门讲用子类特有的属性和方法。

继承的语法

1
2
3
class 类名(父类):
pass

现在我们就以前面的动物类(Animal)与狗类(Dog)说明一下,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
2
3
4
5
汪汪叫

从前面的代码我们可以知道,狗类(Dog)继承自动物类(Animal),现在我们修改一下父类中的代码,例如将print("吃")改为print("吃---")如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
2
3
4
5
吃---
喝---
跑---
睡---
汪汪叫

当我们修改了父类后,子类中的相应方法也出现了变化,并且我们没有修改子类中的任何代码,子类继承自父类的东西,这就是继承的概念,也就是说,子类拥有父类的所有属性和方法。

再看一个案例:

前面提到过,子类扩展了超类的定义。要指定超类,可以在class语句中的类名后加上超类名,并将其用圆括号括起来,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Filter:
def init(self):
self.blocked = []
def filter(self, sequence):
return [x for x in sequence if x not in self.blocked]
class SPAMFilter(Filter): # SPAMFilter是Filter的子类
def init(self): # 重写超类Filter的方法init
self.blocked = ['SPAM']
f = Filter()
f.init()
f.filter([1,2,3])

运行结果如下所示:

1
2
3
4
>>> f = Filter()
>>> f.init()
>>> f.filter([1,2,3])
[1, 2, 3]

Filter是一个过滤序列的通用类。实际上,它不会过滤掉任何东西。 Filter类的用途在于可用作其他类(如将’SPAM’从序列中过滤掉的SPAMFilter类)的基类(超类),如下所示:

1
2
3
s = SPAMFilter()
s.init()
s.filter(['SPAM','SPAM','SPAM','SPAM','eggs','bacon','SPAM'])

运行结果如下所示:

1
2
>>> s.filter(['SPAM','SPAM','SPAM','SPAM','eggs','bacon','SPAM'])
['eggs', 'bacon']

需要注意SPAMFilter类的定义中有两个要点:

第一,以提供新定义的方式重写了Filter类中方法init的定义

第二,直接从Filter类继承了方法filter的定义,因此无需要重新编写其定义。第二点说明了继承很有用的原因:可以创建大量不同的过滤器类,它们都从Filter类派生而来,并且都使用已编写好的方法filter。

深入继承

如果要确定一个类是否是另一个类的子类,可以使用issubclass,如下所示:

1
2
3
4
>>> issubclass(SPAMFilter,Filter)
True
>>> issubclass(Filter,SPAMFilter)
False

如果有一个类,想知道它的基类,可以访问其特殊属性__bases__,(需要注意的是,这里是复数,后来会提到),如下所示;

1
2
3
4
>>> SPAMFilter.__bases__
(<class '__main__.Filter'>,)
>>> Filter.__bases__
(<class 'object'>,)

同样的,如果要确定某个对象是否是特定类的实例,可以使用isinstance,如下所示:

1
2
3
4
5
6
7
>>> s = SPAMFilter()
>>> isinstance(s, SPAMFilter)
True
>>> isinstance(s, Filter)
True
>>> isinstance(s, str)
False

从上面的案例我们可以看出,s是SPAMFilter类的直接实例,但它也是Filter类的间接实例,因为SPMAFilter是Filter的子类,换句话讲,所有SPAMFilter对象都是Filter对象,从前面一个案例可以知道,isinstance也可用于类型,如字符串类型str,如果要想知道对象属于哪个类,可以使用属性__class__,或type,如下所示:

1
2
3
4
>>> s.__class__
<class '__main__.SPAMFilter'>
>>> type(s)
<class '__main__.SPAMFilter'>

相关术语

现在了解一些有关继承的相关术语,还以前面的案例为例说明一下:

  • Dog类是Animal类的子类,Animal类是Dog类的父类,Dog类从Animal类继承。
  • Dog类是Animal类的派生类,Animal类是Dog类的基类,Dog类从Animal类派生。

继承的传递性

  • C类从B类继承,B类又从A类继承;
  • 那么C类就具有B类和A类的所有属性的方法。

子类拥有父类以及父类的父类中封闭的所有属性和方法。

案例分析

现在再看一个简单的案例,继续以前面的案例进行演示,我们先定义一个动物类(Animal),再创建一个动物类(Animal)的子类,即狗类(Dog),再创建一个狗类(Dog)的子类(在这个子类中增加一个叫(bard)的方法),哮天犬类(XiaoTianQuan)(在这个类中,再添加一个飞(fly)的方法),如下所示:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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.fly()
# 使用哮天犬(XiaoTianQuan)这个类中的方法
xtq.bark()
# 使用狗类(Dog)中的叫(bark)这个方法
xtq.sleep()
# 使用动物类(Animal)中的睡觉(sleep)这个方法

结果运行,如下所示:

1
2
3
我会飞
汪汪叫
睡---

从结果我们可以发现,子类中的方法可以由父类继承,也可以由父类的父类继承,这就是继承的传递性,可以从解释器(这里的解释器是PyCharm)中看到这些继承关系:

图1中显示:PyCharm中左侧有一个圆圈,鼠标移过去可以看到,图2中显示:Animal的子类是Dog与XiaoTianQuan,点击一下,显示图3,分别是这几个类。

现在再看一个案例,还是接上面的动物(Animal)类、狗类(Dog)和哮天犬类(XiaoTianQuan)案例,现在我们从动物类中派生出一个猫类(Cat),猫类(Cat)中我们添加一个抓(Catch)这个方法,我们再看一下哮天犬类(XiaoTianQuan)还能否调用猫类(Cat)中的这个方法(Catch),如下所示:

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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("我会飞")
class Cat(Animal):
def catch(self):
print("抓老鼠")
# 创建一个哮天犬对象
xtq = XiaoTianQuan()
xtq.fly()
# 使用哮天犬(XiaoTianQuan)这个类中的方法
xtq.bark()
# 使用狗类(Dog)中的叫(bark)这个方法
xtq.sleep()
# 使用动物类(Animal)中的睡觉(sleep)这个方法
xtq.catch()
# 调用catch这个方法

运行结果如下所示:

1
2
3
4
5
汪汪叫
睡---
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/封装/hm_04_继承的传递注意事项.py", line 44, in <module>
xtq.catch()
AttributeError: 'XiaoTianQuan' object has no attribute 'catch'

运行出错,因为根据继承的传递性,XiaoTianQuan可以继承Dog中的方法,也可以继承Animal中的方法,而Cat是Animal的子类,与Dog是平行关系,XiaoTianQuan无法继承Cat中的方法。

方法的重写

当我们遇到一种情况,例如当父类的方法实现不能满足子类需要时,就需要对方法进行重写(override),例如Animal的子类是Dog,Dog的子类的XiaoTianQuan,而XiaoTianQuan中的叫(bark)这个方法与Dog中的(bark)不一样怎么办,这个时候就需要在XiaoTianQuan类中再次重写bark,如下所示:

重写父类方法有两种情况:

  • 覆盖父类的方法;
  • 扩展父类的方法。

覆盖父类的方法

​ 如果在开发中,父类的方兴未艾实现和子类的方法实现完全不同,就可以采用覆盖的方式,在子类中重新编写父类的方法。具体的实现方法就是在子类中定义一个与父类相同的方法,但代码不同。

重写之后,代码运行时只会调用子类中重写的方法,而不会调用父类中封装的方法,下面看一下案例。

覆盖方法案例分析

先看一下原来的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.bark()
# 使用狗类(Dog)中的叫(bark)这个方法

结果运行如下所示:

1
汪汪叫

在这个案例中,我们创建了一个叫xtqXiaoTianQuan的对象,调用了XiaoTianQuan中的bark方法,如果我们改变了需求,使用哮天犬(xtq)是一种神犬,它叫的方式跟普通的狗不一样,也就是说我们要让xtq中的叫法发生改变,例如由汪汪叫变成了叫得跟神一样...,那么如何实现呢,此时就需要在XiaoTianQuan这个类中重新再次定义bark这个方法,输入以下代码,如下所示:

1
2
def bark(self):
print("叫得跟神一样。。。")

再次运行时,这段代码就会屏蔽父类Dog中的bark方法,只使用XiaoTianQuan类中的方法,完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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("我会飞")
def bark(self):
print("叫得跟神一样...")
# 创建一个哮天犬对象
xtq = XiaoTianQuan()
xtq.bark()
# 使用XiaoTianQuan类中的叫(bark)这个方法

结果如下所示:

1
叫得跟神一样...

从结果中我们就可以看到,xtq中调用了XiaoTianQuan类中重新定义的bark方法。

扩展方法案例分析

现在再看一下重写的另外一种形式,就是对父类方法进行扩展

在实际开发中,有些子类中的方法实现包含了父类的方法实现,并且父类中的这些是子类中这些方法一部分,此时就用到了重写中的扩展形式,它的具体步骤如下所示:

  1. 在子类中重写父类(有的教程称为超类)的方法;
  2. 在需要的位置使用super().父类方法来调用父类中同样的方法
  3. 代码其他的位置针对子类的需求,编写子类中特有的代码实现方式

关于super

此处介绍一下Python中的super

  • super是Python中的一个特殊的类;
  • super()就是使用super类创建出来的对象;
  • 最常用的场景就是在重写父类方法时,调用父类中封装好的方法。

具体案例

现在再来分析一下案例,还以前面的案例说明一下,我们此时又改变了需求,既要求xtq能够像普通狗一样叫(即“汪汪叫”),也能表达出它与普通狗不同的地方,此时们就需要对Dog类中的bark方法进行扩展,因此我们要做以下三点改变:

  1. 针对子类特有的需求编写代码;
  2. 使用super().父类中的方法来调用原本在父类中封装的方法;
  3. 增加其他子类的代码。

也就是说我们要在XiaoTianQuan这个类中重写bark这个方法,并且进行改变,这一部分的代码如下所示:

1
2
3
4
5
6
7
8
9
def bark(self):
# 1. 针对子类特有的需求进行编写
print("神一样的叫唤...")
# 2. 使用super().调用原本在父类中封装的方法
super().bark()
# 3. 增加其他子类的代码
print("#$@#@!@#@$!!@")

完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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("我会飞")
def bark(self):
# 1. 针对子类特有的需求进行编写
print("神一样的叫唤...")
# 2. 使用super().调用原本在父类中封装的方法
super().bark()
# 3. 增加其他子类的代码
print("#$@#@!@#@$!!@")
# 创建一个哮天犬对象
xtq = XiaoTianQuan()
xtq.bark()
# 使用狗类(Dog)中的叫(bark)这个方法

结果运行如下所示:

1
2
3
神一样的叫唤...
汪汪叫
#$@#@!@#@$!!@

从结果中我们可以看到,我们为bark方法添加了新的功能,它不仅继承了原来父类中的功能,还有新的功能。

注意事项

python2.x中调用父类方法中无法使用前面提到的super(),它只能使用父类名.方法(self)来实现,这一区别需要注意,这里不再详述。

父类的私有属性和私有方法

前面我们提到了私有属性私有方法的一些知识:

  • 私有属性、方法是对象的隐私,不对外公开,外界以及子类都不能直接访问;
  • 私有属性、方法通常用于做一些内部的事情。

从原来的知识点中我们知道了:

  1. 子类对象不能在自己的方法内部直接访问父类的私有属性或私有方法
  2. 子类对象可以通过父类的公有方法间接访问到私有属性或私有方法

先看下面的一张示意图:

从上面的示意图我们知道,A是父类,BA的子类,其中:

  • B的对象不能直接访问__num2属性;
  • B的对象不能在demo方法内访问__num2的属性;
  • B的对象可以在demo方法内调用父类的test方法;
  • 父类的test方法内部,能够访问__num2属性和__test方法。

案例分析1——子类无法直接访问父类的属性与方法

现在用代码来分析一下上面提到的父类私有属性和私有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200
def __test(self):
print("私有方法 %d %d" %(self.num1, self.__num2))
class B(A):
pass
# 创建一个子类对象
b = B()
print(b)
# 在外界不能直接访问对象的私有属性/调用私有方法
print(b.__num2)
b.__test()

代码运行如下所示:

1
2
3
4
5
Traceback (most recent call last):
<__main__.B object at 0x00000170DA378BA8>
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/封装/hm_07_父类的私有属性和私有方法.py", line 20, in <module>
print(b.__num2)
AttributeError: 'B' object has no attribute '__num2'

结果出错。因此可以看出来,虽然B的父类是A,但在B中也无法调用A的私有属性和方法。

再修改一下代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200
def __test(self):
print("私有方法 %d %d" %(self.num1, self.__num2))
class B:
def demo(self):
# 1. 在子类的对象方法中,无法访问父类的私有属性
print("访问父类的私有属性 %d" % self.__num2)
# 2. 在子类的对象方法中,无法调用父类的私有方法
self.__test()
# 创建一个子类对象
b = B(A)
print(b)
# 在外界不能直接访问对象的私有属性/调用私有方法
print(b.__num2)
b.__test()

结果运行仍然出错,如下所示:

1
2
3
4
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/封装/hm_07_父类的私有属性和私有方法.py", line 23, in <module>
b = B(A)
TypeError: object() takes no parameters

在子类中无法调用父类的方法。

案例分析2——间接访问父类私有属性,调用私有方法

虽然子类不能在自己的方法内部直接访问父类的私有属性和私有方法,但是可以访问其公有属性与公有方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200
def __test(self):
print("私有方法 %d %d" %(self.num1, self.__num2))
def test(self):
print("父类的公有方法")
class B(A):
def demo(self):
pass
b = B() # 创建一个子类对象
print(b) # 输出b的内存地址
print(b.num1) # 输出B的父类的公有属性num1
b.test() # 调用父类的公有方法

结果运行如下所示:

1
2
3
<__main__.B object at 0x000002291BB00550>
100
父类的公有方法

从结果我们可以知道,子类可以访问父类的公有属性,调用公有方法。

现在我们继续看案例,在子类B中的方法demo()能否访问父类的公有属性,调用父类的公有方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200
def __test(self):
print("私有方法 %d %d" %(self.num1, self.__num2))
def test(self):
print("父类的公有方法")
class B(A):
def demo(self):
# 1. 在子类的对象方法中,不能访问父类的私有属性
# print("访问父类的私有属性 %d" % self.__num2)
# 2. 在子类的对象方法中,不能调用父类的私有方法
# self.__test()
# 3. 访问父类的公有属性
print("子类方法 %d" % self.num1)
# 4. 调用父类的公有方法
self.test()
pass
b = B() # 创建一个子类对象
b.demo()
# print(b) # 输出b的内存地址
# print(b.num1) # 输出B的父类的公有属性num1
# b.test() # 调用父类的公有方法

从结果中我们可以看到,子类中可以调用父类中的公有方法。

再继续看案例,在这个案例中,我们会看到:①在父类中,通过父类的公用方法(假设是abc这个方法)调用父类私有方法或访问父类的私有属性;②在子类中,调用父类的这个公有方法(就是abc这个方法)。

这种情况会是什么样子,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200
def __test(self):
print("私有方法 %d %d" %(self.num1, self.__num2))
def test(self):
print("父类的公有方法 %d" % self.__num2)
# 在父类中,test()是一个公有方法,而__num2是一个私有属性
self.__test()
class B(A):
def demo(self):
# 1. 在子类的对象方法中,不能访问父类的私有属性
# print("访问父类的私有属性 %d" % self.__num2)
# 2. 在子类的对象方法中,不能调用父类的私有方法
# self.__test()
# 3. 访问父类的公有属性
print("子类方法 %d" % self.num1)
# 4. 调用父类的公有方法
self.test()
pass
b = B() # 创建一个子类对象
b.demo()
# print(b) # 输出b的内存地址
# print(b.num1) # 输出B的父类的公有属性num1
# b.test() # 调用父类的公有方法

结果运行如下所示:

1
2
3
子类方法 100
父类的公有方法 200
私有方法 100 200

从结果中我们可以看到:

  1. bB的对象,BA的子类;
  2. b无法直接访问A的私有属性,也无法直接调用A的私有方法;
  3. A中定义了一个公有方法test(),这个公有方法中调用了私有方法__test(),访问了私有属性__num2
  4. b中调用了A的公有方法test(),因此b也能调用A的私有方法__test(),访问了私有属性__num2,这种途径是间接实现的,而非直接实现的,相当于使用test()来做了一个中转。

多继承

单继承与多继承

前面我们提到的案例都是单继承,也就是一个子类只有一个父类,在这一部分中,我们会涉及到多继承:

  • 子类可以拥有多个父类,并且具有所有父类属性方法
  • 例如:孩子会继承息父类和母亲的特性,如下所示:

多继承的语法

多继承的语法如下所示:

1
2
class 子类名(父类名1, 父类名2...)
pass

多继承案例分析

现在我们根据前面的多继承示意图来看一个案例,在这个案例中,我们定义2个父类,分别是AB,在A中定义一个test()方法,在B中定义一个demo()方法,然后再定义一个子类,即C类,让C类继承A类和B类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()

代码运行如下所示:

1
2
test 方法
demo 方法

从结果中我们可以发现,C既可以调用A中的test()方法,也能调用B中的demo()方法。多继承可以在很大程度上节省代码量。

多继承的注意事项

在使用多继承的时候,我们可以会遇到这样的问题:如果不同的父类中存在同名的方法,子类对象在调用方法时,会调用哪一个父类的方法呢?就像下面的这个样子:

A中有test()方法,B中也有test()方法,如果C同时继承A与B,那么在调用test()方法时,就会有问题。

我们来分析一下这种情况,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class A:
def test(self):
print("A --- test 方法")
def demo(self):
print("A --- demo 方法")
class B:
def demo(self):
print("B --- demo 方法")
def test(self):
print("B --- test 方法")
class C(A, B):
pass
c = C()
c.test()
c.demo()

结果运行如下所示:

1
2
A --- test 方法
A --- demo 方法

从结果中可以发现,C中的test()demo()方法都继承自A。从class C(A,B)这段代码中我们可以知道,C是先继承自A,再继承自B,因此,如果遇到相同名称的方法,那么就是先AB,如果我们把代码改为class C(B,A),那么运行结果就是下面的这个样子:

1
2
B --- test 方法
B --- demo 方法

此时就会发现,C中的方法都继承自B。但Python中子类关于父类的继承顺序并不是如此简单,举这个例子主要是为了说明,当两个父类中含有相同名称的方法时,慎重选择多继承。

MRO

MRO全称是Method Resolution Order,即方法解析顺序。它用于在多继承时判断方法、属性的调用路径。Python中针对类提供了一个内置属性__mro__用于查看方法搜索顺序。

在前面的代码中补充一句print(C.__mro__),结果如下所示:

1
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

从结果中可以看出以下信息:

  1. 输出结果是一个元组;
  2. 元组中的顺序就是一些类,分别是C,B,A,原来结果中的运行原理就是,当我们使用c=C()来创建一个C类的c对象时,如果有C类,那么就调用C类中的方法,如果没有,就按照从左到右的顺序进行搜索,也就是按C,B,A这三个类中方法进行调用,的顺序来进行调用。
  3. 最后一个是<class 'object'>,它是Python3中的所有类的基类,也就是说,只要我们定义了一个类,所有类的基类都是<class 'object'>,此处先不详述。
  4. 如果查找到最后一个基类,还没有找到相应的类,python就会报错。

多个超类

前面我们说了,__bases__是复数形式,我们可以用它来知道类的基类,而基类可能有多个,为说明如何继承多个类,下面我们来看一下案例,如下所示:

1
2
3
4
5
6
7
8
9
10
class Calculatro:
def calculate(selfself, expression):
self.value = eval(expression)
class Talker:
def talk(self):
print("Hi, my value is ", self.value)
class TalkingCalculator(Calculator, Talker):
pass

子类TalkingCalculator本身无所作为,其所有的行为都是从超类那里继承的,关键是通过Calculator那里继承calculate,并从Talker那里继承talk,它能了会说话的计算器,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Calculator:
def calculate(self, expression):
self.value = eval(expression)
class Talker:
def talk(self):
print("Hi, my value is ", self.value)
class TalkingCalculator(Calculator, Talker):
pass
tc = TalkingCalculator()
tc.calculate('1 + 2 *3')
tc.talk()

结果如下所示:

1
2
3
4
>>> tc = TalkingCalculator()
>>> tc.calculate('1 + 2 *3')
>>> tc.talk()
Hi, my value is 7

这种情况称为多重继承,这是一种功能强大的工具,然而除非万不得已,尽量避免使用这种方法。在使用多重继承时,有一点需要注意:如果多个超类以不同的方式实现了同一个方法(即有多个同名方法),必须在class语句中仔细排列这些超类,因此位于前面的类的方法会覆盖位于后面的类的方法。因此,在前面的救命中,如何Calculator类包含方法talk,那么这个方法将覆盖Talker类的方法talk,导致它不可访问,如果像下面这样反转超类的排列顺序,如下所示:

1
class TalkingCalculator(Talker, Calculator):pass

这将导致Talker的方法talk是可以访问的,多个超类的超类相同时,查找特定方法或属性时访问超类的顺序称为方法解析顺序(MRO)。

新式类与旧式(经典)类

上面提到,object是Python为所有对象提供的基类,提供有一些内置的属性和方法,可以使用dir函数查看。

Python中有新式类与旧式类区分,其中:

  • 新式类:以object为基类的类,推荐使用;
  • 经典类:不以object为基类的类,不推荐使用。
  • Python 3.x中定义的类,如果没有指定父类,会默认使用object作为该类的基类,Python 3.x中定义的类都是新式类;
  • Python 2.x中定义类时,如果没有指定父类,则不会以object作为基类。
  • 新式类和经典类在多继承时会影响到方法的搜索顺序。
  • 为了保证编写的代码能够同时在Python 2.xPython 3.x中同时运行,在定义类时,如果没有父类,建议统一继承自object类。

现在我们看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
In [1]: class A(object):
...: pass
...:
In [2]: a = A()
In [3]: dir(a)
Out[3]:
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__']

在上面的代码中我们可以知道,我们开始定义了一个A类,代码为class A(object):,从这里可以看出来,这是一个新式类,然后我们使用了dir(a)来查看这个类的方法,可以看到许多方法,但是,如果在Python3中,即使你输入的是class A():,没有指定继承自object,那么A类也是新式类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
In [5]: class B:
...: pass
...:
In [6]: b = B()
In [7]: dir(b)
Out[7]:
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__']