Python 系列学习之九:面向对象编程 - 继承和多态

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;本章主要介绍 Python 有关面向对象编程中的继承和多态;

本文为作者的原创作品,转载需注明出处;

继承和多态

本章节,笔者将会继续使用类的定义小节中所使用到的 Student 类,不过稍加修改该,额外提供一个实例方法,say_hello 方法;

1
2
3
4
5
6
7
8
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('Student name is %s and sore is %s' % (self.name, self.score))
def say_hello(self):
print('Hello everyone, I\'m a student')

继承

笔者将以 Student 为基础类并扩展出相应的子类,小学生( Primary Student )、中学生( Middle School Student )和大学生( Colleage Student ),

primary student

1
2
class PrimaryStudent(Student):
pass

middle school student

1
2
class MiddleSchoolStudent(Student):
pass

colleage student

1
2
class ColleageStudent(Student):
pass

下面,我们就来测试一下,相关子类的继承的特性,以 PrimaryStudent 的相关测试用例为例,

1
2
3
4
5
>>> cindy = PrimaryStudent('Cindy Lawuer', 40)
>>> cindy.say_hello()
Hello everyone, I'm a student
>>> cindy.print_score()
Student name is Cindy Lawuer and sore is 40

可见,默认情况下,子类会继承父类所有的成员变量和方法;

多态

继承小节中我们清晰的看到,子类继承了父类的特性(成员变量和方法);那么问题来了,我该怎样去定义子类自有的特性(成员变量和方法)呢?可以的,答案就是覆盖

覆盖

这里,我们通过子类覆盖父类的方法 say_hello 为例子来演示多态的特性;

primary student

1
2
3
class PrimaryStudent(Student):
def say_hello(self):
print('Hello everyone, I\'m a primary student')

middle school student

1
2
3
class MiddleSchoolStudent(Student):
def say_hello(self):
print('Hello everyone, I\'m a middle school student')

colleage student

1
2
3
class ColleageStudent(Student):
def say_hello(self):
super.print('Hello everyone, I\'m a colleage student')

下面,笔者将展示子类特有的特性,

1
2
3
>>> cindy = PrimaryStudent('Cindy Lawuer', 40)
>>> cindy.say_hello()
Hello everyone, I'm a primary student

这样,通过子类覆盖父类的特性(成员变量或方法)的方式,实现了子类的多态的特性;

super

普通方法

覆盖虽然为面向对象编程带来了多态的特性,但是,却引入了另外一个问题,就是子类把父类的方法覆盖掉以后,在某些特殊的时候,子类依然需要调用被子类所覆盖了的父类方法,该怎么办呢?笔者将继续使用上述的例子,试图在 PrimaryStudent 的实例中去调用被覆盖的父类方法 say_hello

由于习惯了 Java 语言,自然想到了如下的调用方式,

1
2
3
class PrimaryStudent(Student):
def say_hello(self):
super.say_hello('Hello everyone, I\'m a primary student')

初始化 PrimaryStudent,执行,

1
2
3
4
5
6
>>> cindy = PrimaryStudent('Cindy Lawuer', 40)
>>> cindy.say_hello()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in say_hello
AttributeError: type object 'super' has no attribute 'say_hello'

却告诉我,super 对象没有属性 say_hello;看来,我们不能够简单的直接通过 super 关键字来调用父类的方法了;查阅了相关资料,发现,如果要在子类中调用父类的方法(子类覆盖过的父类方法),首先需要构造出父类的引用,然后通过该引用去掉用父类的方法;所以,修改该如下,

1
2
3
4
class PrimaryStudent(Student):
def say_hello(self):
parent = super(PrimaryStudent, self)
parent.say_hello()

执行,

1
2
3
>>> cindy = PrimaryStudent('Cindy Lawuer', 40)
>>> cindy.say_hello()
Hello everyone, I'm a student

可见,我们在子类中成功的调用了被覆盖了的父类的方法;当然,如果要调用未被子类覆盖的方法,直接通过子类实例调用即可;

1
2
>>> cindy.print_score()
Student name is Cindy Lawuer and sore is 40

构造方法

还有一个非常常用的场景就是调用父类的构造方法,为了便于演示,我们进一步修改 Student 类,

1
2
3
4
5
6
7
8
9
10
11
12
class Student(object):
def __init__(self, name, score):
self.name = name
# 初始化 score
if( score < 0 ):
self.init_score()
else:
self.score = score
def print_score(self):
print('Student name is %s and sore is %s' % (self.name, self.score))
def init_score(self):
self.score = 60

这样,我们对 Student 的构造函数做了调整,如果错误的分数(负数),将分数设置为默认分数 60;

下面,我们一步一步的来梳理 Python 构造函数的特性,

首先,定义一个没有构造函数的子类 PrimaryStudent,

PrimaryStudent

1
2
class PrimaryStudent(Student):
pass

再次,构造 PrimaryStudent 实例 cindy

1
2
3
>>> cindy = PrimaryStudent('Cindy Lawuer', -1)
>>> cindy.print_score()
Student name is Cindy Lawuer and sore is 60

可见,当子类没有构造函数的情况下,将沿用父类的构造函数;那么,如果子类需要由自己特定的构造函数来初始化一些特殊的逻辑呢?

首先,我们重新定义 PrimaryStudent 并提供相关的构造函数,该构造函数中初始化了年龄范围,

1
2
3
4
5
6
7
class PrimaryStudent(Student):
def __init__(self, name, score):
self.name = name
self.score = score
self.age_range = [6,13]
def print_age_range(self):
print("the age range of primary student is " + str(self.age_range) )

再试试,

1
2
3
4
5
>>> cindy = PrimaryStudent('Cindy Lawuer', -1)
>>> cindy.print_age_range()
the age range of primary student is [6, 13]
>>> cindy.print_score()
Student name is Cindy Lawuer and sore is -1

通过 PrimaryStudent 构造函数,我么构其特有的属性既年龄范围[6, 13];但是,因为重载了父类的构造函数,因此没有了自动规避错误分数的问题,这里我们得到了错误分数 -1,可是这个问题明明在我们的父类 Student 中得到过解决的;其实,构造函数__init__普通函数本质上没什么区别,都是函数,所以,子类同样可以覆盖父类的“同参”构造函数,这个例子中,正好 PrimaryStudent 覆盖了父类 Student 的构造函数 __init__(self, name, score),所以,正确子类的构造函数是需要继承父类构造函数的逻辑的,而这个继承,也就是通过与调用普通函数父类方法类似的办法去调用的;所以,将 PrimaryStudent 修改如下,

1
2
3
4
5
6
7
class PrimaryStudent(Student):
def __init__(self, name, score):
parent = super(PrimaryStudent, self)
parent.__init__(name, score)
self.age_range = [6,13]
def print_age_range(self):
print("the age range of primary student is " + str(self.age_range) )

再试试,

1
2
3
>>> cindy = PrimaryStudent('Cindy Lawuer', -1)
>>> cindy.print_score()
Student name is Cindy Lawuer and sore is 60

可见,通过在子类构造函数中调用父类的构造函数以后,规避错误分数的默认方式在子类 PrimaryStudent 中得到了实现;

写在最后

写到这里,就语法学习本身而言,本来就该结束了;可是笔者却在思考的是,super(PrimaryStudent, self) 里面到底发生了什么?super 方法需要 PrimaryStudent 的 class 引用以及 self( this 指针 )作为获取父类的引用的参数;等等,有个关键的地方,获取父类的引用?这里的表述是不清楚的,这里实际上需要得到一个父类的实例的引用才对,是呀,我们应该得到的是一个父类的实例才对,否则怎么引用父类的实例方法 say_hello() 呢,分析到这里,笔者便恍然大悟了,super(PrimaryStudent, self) 试图去构造一个与 PrimaryStudent 相关的父类 Student 实例,其相关参数的涵义如下,

  1. PrimaryStudent
    这个参数在 Python 的内置函数 super 中被调用,它的作用就是告诉 Python 编译器我的父类是谁?Python 编译器通过解释 PrimaryStudent 便可以找到其父类 Student;

  2. self 的作用是什么呢?
    很容易想到,就是去提供构造父类实例的时候所需要的上下文环境,比如构造父类实例的时候所需要使用到的构造参数等等;

另外,要特别注意,在子类构造方法中调用父类构造方法中,self 的作用范围,比如构造方法小节中,在子类 PrimaryStudent 中定义的构造函数,

1
2
3
4
def __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;