前言
打算写一系列文章来记录自己学习 Python 3 的点滴;本章主要介绍 Python 有关面向对象编程中的继承和多态;
本文为作者的原创作品,转载需注明出处;
继承和多态
本章节,笔者将会继续使用类的定义小节中所使用到的 Student 类,不过稍加修改该,额外提供一个实例方法,say_hello 方法;
1 | class Student(object): |
继承
笔者将以 Student 为基础类并扩展出相应的子类,小学生( Primary Student )、中学生( Middle School Student )和大学生( Colleage Student ),
primary student1
2class PrimaryStudent(Student):
pass
middle school student1
2class MiddleSchoolStudent(Student):
pass
colleage student1
2class ColleageStudent(Student):
pass
下面,我们就来测试一下,相关子类的继承的特性,以 PrimaryStudent 的相关测试用例为例,
1 | 'Cindy Lawuer', 40) cindy = PrimaryStudent( |
可见,默认情况下,子类会继承父类所有的成员变量和方法;
多态
从继承小节中我们清晰的看到,子类继承了父类的特性(成员变量和方法);那么问题来了,我该怎样去定义子类自有的特性(成员变量和方法)呢?可以的,答案就是覆盖;
覆盖
这里,我们通过子类覆盖父类的方法 say_hello 为例子来演示多态的特性;
primary student1
2
3class PrimaryStudent(Student):
def say_hello(self):
print('Hello everyone, I\'m a primary student')
middle school student1
2
3class MiddleSchoolStudent(Student):
def say_hello(self):
print('Hello everyone, I\'m a middle school student')
colleage student1
2
3class ColleageStudent(Student):
def say_hello(self):
super.print('Hello everyone, I\'m a colleage student')
下面,笔者将展示子类特有的特性,
1 | 'Cindy Lawuer', 40) cindy = PrimaryStudent( |
这样,通过子类覆盖父类的特性(成员变量或方法)的方式,实现了子类的多态
的特性;
super
普通方法
覆盖虽然为面向对象编程带来了多态的特性,但是,却引入了另外一个问题,就是子类把父类的方法覆盖掉以后,在某些特殊的时候,子类依然需要调用被子类所覆盖了的父类方法,该怎么办呢?笔者将继续使用上述的例子,试图在 PrimaryStudent 的实例中去调用被覆盖的父类方法 say_hello,
由于习惯了 Java 语言,自然想到了如下的调用方式,
1 | class PrimaryStudent(Student): |
初始化 PrimaryStudent,执行,
1 | 'Cindy Lawuer', 40) cindy = PrimaryStudent( |
却告诉我,super 对象没有属性 say_hello;看来,我们不能够简单的直接通过 super 关键字来调用父类的方法了;查阅了相关资料,发现,如果要在子类中调用父类的方法(子类覆盖过的父类方法),首先需要构造出父类的引用,然后通过该引用去掉用父类的方法;所以,修改该如下,
1 | class PrimaryStudent(Student): |
执行,
1 | 'Cindy Lawuer', 40) cindy = PrimaryStudent( |
可见,我们在子类中成功的调用了被覆盖了的父类的方法;当然,如果要调用未被子类覆盖的方法,直接通过子类实例调用即可;
1 | cindy.print_score() |
构造方法
还有一个非常常用的场景就是调用父类的构造方法,为了便于演示,我们进一步修改 Student 类,
1 | class Student(object): |
这样,我们对 Student 的构造函数做了调整,如果错误的分数(负数),将分数设置为默认分数 60;
下面,我们一步一步的来梳理 Python 构造函数的特性,
首先,定义一个没有构造函数的子类 PrimaryStudent,
PrimaryStudent1
2class PrimaryStudent(Student):
pass
再次,构造 PrimaryStudent 实例 cindy,
1 | 'Cindy Lawuer', -1) cindy = PrimaryStudent( |
可见,当子类没有构造函数的情况下,将沿用父类的构造函数;那么,如果子类需要由自己特定的构造函数来初始化一些特殊的逻辑呢?
首先,我们重新定义 PrimaryStudent 并提供相关的构造函数,该构造函数中初始化了年龄范围,
1 | class PrimaryStudent(Student): |
再试试,
1 | 'Cindy Lawuer', -1) cindy = PrimaryStudent( |
通过 PrimaryStudent 构造函数,我么构其特有的属性既年龄范围[6, 13];但是,因为重载了父类的构造函数,因此没有了自动规避错误分数的问题,这里我们得到了错误分数 -1,可是这个问题明明在我们的父类 Student 中得到过解决的;其实,构造函数__init__
与普通函数本质上没什么区别,都是函数,所以,子类同样可以覆盖父类的“同参”构造函数,这个例子中,正好 PrimaryStudent 覆盖了父类 Student 的构造函数 __init__(self, name, score),所以,正确子类的构造函数是需要继承父类构造函数的逻辑的,而这个继承,也就是通过与调用普通函数父类方法类似的办法去调用的;所以,将 PrimaryStudent 修改如下,
1 | class PrimaryStudent(Student): |
再试试,
1 | 'Cindy Lawuer', -1) cindy = PrimaryStudent( |
可见,通过在子类构造函数中调用父类的构造函数以后,规避错误分数的默认方式在子类 PrimaryStudent 中得到了实现;
写在最后
写到这里,就语法学习本身而言,本来就该结束了;可是笔者却在思考的是,super(PrimaryStudent, self) 里面到底发生了什么?super 方法需要 PrimaryStudent 的 class 引用以及 self( this 指针 )作为获取父类的引用的参数;等等,有个关键的地方,获取父类的引用?这里的表述是不清楚的,这里实际上需要得到一个父类的实例
的引用才对,是呀,我们应该得到的是一个父类的实例才对,否则怎么引用父类的实例方法 say_hello() 呢,分析到这里,笔者便恍然大悟了,super(PrimaryStudent, self) 试图去构造一个与 PrimaryStudent 相关的父类 Student 实例,其相关参数的涵义如下,
PrimaryStudent
这个参数在 Python 的内置函数 super 中被调用,它的作用就是告诉 Python 编译器我的父类是谁?Python 编译器通过解释 PrimaryStudent 便可以找到其父类 Student;那 self 的作用是什么呢?
很容易想到,就是去提供构造父类实例的时候所需要的上下文环境,比如构造父类实例的时候所需要使用到的构造参数等等;
另外,要特别注意,在子类构造方法中调用父类构造方法中,self 的作用范围,比如构造方法小节中,在子类 PrimaryStudent 中定义的构造函数,1
2
3
4def __init__(self, name, score):
parent = super(PrimaryStudent, self)
parent.__init__(name, score)
self.age_range = [6,13]
其中,通过 parent.__init__(name, score) 调用父类的构造函数的内部,self
使用的是子类的 PrimaryStudent,所以,在调用父类的构造函数的上下文环境,使用的是子类 PrimaryStudent,所以,其作用范围自然作用到的就是子类 PrimaryStudent;