3.4 面向对象编程
面向对象编程(Object Oriented Programming,OOP)是一种程序设计思想。它把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
面向过程的程序设计把计算机程序视为一系列命令的集合,即一组函数的顺序执行。为了简化程序设计,面向过程的程序设计把函数继续切分为子函数,即把大块函数通过切分成小块函数来降低系统的复杂度。
而面向对象的程序设计把计算机程序视为一组对象的集合,每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行过程就是一系列消息在各个对象之间传递的过程。
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象就是面向对象中的类(class)。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个字典表示。
std1 = { 'name': 'Storm', 'score': 100 } std2 = { 'name': 'SK', 'score': 81 }
而处理学生成绩可以通过函数实现,例如输出学生的成绩。
def print_score(std): print('{}: {}'.format(std['name'],std['score']))
如果采用面向对象的程序设计思想,我们首先应该思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(property)。如果要输出一个学生的成绩,首先必须创建出这个学生对应的对象,然后给对象发一个print_score消息,让对象输出自己的数据(test3_15.py)。
class Student(object): '''定义一个Student对象,包含name和score两个属性''' def __init__(self, name, score): self.name = name self.score = score def print_score(self): print('{}: {}' .format(self.name, self.score))
给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(method)。面向对象的程序如下所示。
storm = Student('Storm', 59) sk = Student('SK', 87) storm.print_score() sk.print_score()
面向对象的设计思想是从自然界中来的,因为在自然界中,类和实例(instance)是很自然的概念。类是一种抽象概念,例如我们定义的Student类,是指学生这个概念;而实例则是一个个具体的Student,如storm和sk是两个具体的Student。
所以,面向对象的设计思想是抽象出类,根据类创建实例。
面向对象的抽象程度又比函数要高,因为一个类既包含数据,又包含操作数据的方法。
3.4.1 类和实例
面向对象中最重要的概念就是类和实例,必须牢记类是抽象的模板,例如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
(1)定义类
仍以Student类为例,在Python中,定义类是通过class关键字实现的。
class Student(object): pass
class后面紧接着的是类名,即Student,类名通常是大写开头的单词;紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过“类名+()”实现的。
>>> storm = Student() >>> storm <__main__.Student object at 0x10a67a590> >>> Student <class '__main__.Student'>
可以看到,变量storm指向的就是一个Student的实例,后面的0x10a67a590是内存地址,每个object的地址都不一样,而Student本身则是一个类。
可以自由地给一个实例变量绑定属性,例如,给实例storm绑定一个name属性。
>>> storm.name = 'Storm Spirit' >>> storm.name 'Storm Spirit'
由于类可以起到模板的作用,因此可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就可以把name和score等属性绑定上去。
class Student(object): def __init__(self, name, score): self.name = name self.score = score
需要注意以下几点。
●特殊方法__init__前后分别有两条下划线。
●__init__方法的第一个参数永远是self,表示创建的实例本身。因此,在__init__方法内部,可以把各种属性绑定到self,因为self指向创建的实例本身。
●有了__init__方法,在创建实例的时候就不能传入空的参数了,必须传入与__init__方法匹配的参数。但self不需要传入,Python解释器自己会把实例变量传进去。
>>> storm = Student('Storm Spirit', 100) >>> storm.name 'Storm Spirit' >>> storm.score 100
和普通的函数相比,在类中定义的方法只有一点不同,就是第一个参数永远是实例变量self,并且在调用时不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以仍然可以使用默认参数、可变参数、关键字参数和命名关键字参数。
(2)数据封装
面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例都拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,例如输出一个学生的成绩。
>>> def print_score(std): ... print('%s: %s' % (std.name, std.score)) ... >>> print_score(storm) Storm Spirit: 100
但是,既然Student实例本身就拥有这些数据,那么就没有必要通过外面的函数去访问这些数据,可以直接在Student类的内部定义访问数据的函数,这样就把数据给封装起来了。这些封装数据的函数和Student类本身是关联起来的,我们称之为类的方法。
class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print('%s: %s' % (self.name, self.score))
要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传入,其他参数正常传入。
>>> storm.print_score() Storm Spirit: 100
这样一来,我们从外部看Student类,就只需要知道创建实例需要给出name和score。而和输出相关的操作都是在Student类的内部定义的,这些数据和逻辑被封装起来了,调用起来很容易,且不用知道内部实现的细节。
封装的另一个好处是可以给Student类增加新的方法,例如get_grade。
class Student(object): ... def get_grade(self): if self.score >= 90: return 'A' elif self.score >= 60: return 'B' else: return 'C'
同样,get_grade方法可以直接在实例变量上调用,不需要知道内部实现细节。
class Student(object): def __init__(self, name, score): self.name = name self.score = score def get_grade(self): if self.score >= 90: return 'A' elif self.score >= 60: return 'B' else: return 'C'
●类是创建实例的模板,而实例则是一个个具体的对象,各个实例拥有的数据都互相独立,互不影响。
●方法就是与实例绑定的函数。和普通函数不同,方法可以直接访问实例的数据。
●通过在实例上调用方法,我们就直接操作了对象内部的数据,但无须知道方法内部的实现细节。
●和静态语言不同,Python允许对实例变量绑定任何数据。也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称可能不同。
3.4.2 继承和多态
在面向对象程序设计中,当我们定义了一个类时,可以从之前的某个类继承。被继承的类称为父类或者基类、超类,新创建的类称为子类。
例如,我们创建了一个名为Animals的类,它有一个run方法,如下所示。
class Animals(object): def run(self): print('Animals can run')
当我们需要编写Dog和Cat类时,可以从Animals类继承(狗和猫正好是动物,也都会跑)。
class Animals(object): def run(self): print('Animals can run') class Dog(Animals): pass class Cat(Animals): pass
对于Dog和Cat来说,Animals是父类,Dog和Cat此时是子类。继承有什么好处呢?好处就是,Dog和Cat这两个类什么代码都没写(除了pass)就有了run方法(test3_16.py)。
class Animals(object): def run(self): print('Animals can run') class Dog(Animals): pass class Cat(Animals): pass if __name__ == '__main__': Dog().run() Cat().run()
运行结果如下。
C:\Python\Python36\python.exe D:/Love/Chapter_3/test3_16.py Animals can run Animals can run Process finished with exit code 0
当然,我们也可以给子类新定义一些方法,例如,我们可以让Dog“汪汪汪”地叫。
class Animals(object): def run(self): print('Animals can run') class Dog(Animals): def say(self): print(“汪汪汪”) class Cat(Animals): pass if __name__ == '__main__': Dog().run() Dog().say() Cat().run()
运行结果如下。
Animals can run 汪汪汪 Animals can run
Animals can run
如果觉得Dog跑的方法和Cat跑的方法应该不同,那么我们可以对代码进行改进。
class Animals(object): def run(self): print('Animals can run') class Dog(Animals): def run(self): print('Dog can run') def say(self): print(“汪汪汪”) class Cat(Animals): def run(self): print('Cat can run') if __name__ == '__main__': Dog().run() Dog().say() Cat().run()
运行结果如下。
Dog can run 汪汪汪 Cat can run
当子类和父类都存在相同的方法run时,子类run方法的效果会覆盖父类run方法的效果,这就是多态的效果。
数据封装、继承和多态是面向对象的三大特点。继承可以把父类的所有功能都拿来用,这样就不必“重新来过”,子类只需要新增自己特有的方法即可。当然子类也可以把父类的某个方法覆盖掉,让继承呈现多态的效果。