⾯向对象编程是⼀种⾮常流⾏的
编程范式
(
programming paradigm
),所谓编程范式就是
程序设计的
⽅法学
,也就是程序员对程序的认知和理解。
前⾯的课程中我们说过“
程序是指令的集合
”
,运⾏程序时,程序中的语句会变成⼀条或多条指令,然后由CPU
(中央处理器)去执⾏。为了简化程序的设计,我们⼜讲到了函数,
把相对独⽴且经常重复使⽤
的代码放置到函数中
,在需要使⽤这些代码的时候调⽤函数即可。如果⼀个函数的功能过于复杂和臃肿,我们⼜可以进⼀步将函数进⼀步拆分为多个⼦函数
来降低系统的复杂性。
不知⼤家是否发现,我们所谓的编程其实是写程序的⼈按照计算机的⼯作⽅式通过代码控制机器完成任务。但是,计算机的⼯作⽅式与⼈类正常的思维模式是不同的,如果编程就必须抛弃⼈类正常的思维⽅式去迎合计算机,编程的乐趣就少了很多,⽽“
每个⼈都应该学习编程
”
这样的豪⾔壮语也就只能喊喊⼝号⽽已。不是说我们不能按照计算机的⼯作⽅式去编写代码,但是当我们需要开发⼀个复杂的系统时,这种⽅式会让代码过于复杂,从⽽导致开发和维护⼯作都变得举步维艰,这也就是上世纪60
年代末,出现了“
软件危机
”
、
“
软件⼯程
”
这些概念的原因。
随着软件复杂性的增加,解决
“
软件危机
”
就成了软件开发者必须直⾯的问题。诞⽣于上世纪
70
年代的Smalltalk语⾔让软件开发者看到了希望,因为它引⼊了⼀种新的编程范式叫⾯向对象编程。在⾯向对象编程的世界⾥,程序中的数据和操作数据的函数是⼀个逻辑上的整体
,我们称之为
对象
,
对象可以接收
消息
,解决问题的⽅法就是
创建对象并向对象发出各种各样的消息
;通过消息传递,程序中的多个对象可以协同⼯作,这样就能构造出复杂的系统并解决现实中的问题。当然,⾯向对象编程的雏形还可以向前追溯到更早期的Simula
语⾔,但这不是我们现在要讨论的重点。
说明:
今天我们使⽤的很多⾼级程序设计语⾔都⽀持⾯向对象编程,但是⾯向对象编程也不是解
决软件开发中所有问题的
“
银弹
”
,或者说在软件开发这个⾏业⽬前还找不到这种所谓的
“
银弹
”
。
类和对象
如果要⽤⼀句话来概括⾯向对象编程,我认为下⾯的说法是相当精准的。
⾯向对象编程
:把⼀组数据和处理数据的⽅法组成
对象
,把⾏为相同的对象归纳为
类
,通过
封装隐藏对象的内部细节,通过继承
实现类的特化和泛化,通过
多态
实现基于对象类型的动态分派。
这句话对初学者来说可能难以理解,但是我们先为⼤家圈出⼏个关键词:对象
(
object
)、
类(class
)、
封装
(
encapsulation
)、
继承
(
inheritance
)、
多态
(
polymorphism
)。
我们先说说类和对象这两个词。在⾯向对象编程中,
类是⼀个抽象的概念,对象是⼀个具体的概念
。我们把同⼀类对象的共同特征抽取出来就是⼀个类,⽐如我们经常说的⼈类,这是⼀个抽象概念,⽽我们每个⼈就是⼈类的这个抽象概念下的具体的实实在在的存在,也就是⼀个对象。简⽽⾔之,类是对象的
蓝图和模板,对象是类的实例
。
在⾯向对象编程的世界中,
⼀切皆为对象
,
对象都有属性和⾏为
,
每个对象都是独⼀⽆⼆的
,⽽且
对象
⼀定属于某个类
。对象的属性是对象的静态特征,对象的⾏为是对象的动态特征。按照上⾯的说法,如果我们把拥有共同特征的对象的属性和⾏为都抽取出来,就可以定义出⼀个类。
![](https://i-blog.csdnimg.cn/blog_migrate/74e806f73913cf962b57377c19ddacd6.png)
定义类
在
Python
中,可以使⽤
class
关键字加上类名来定义类,通过缩进我们可以确定类的代码块,就如同定义函数那样。在类的代码块中,我们需要写⼀些函数,我们说过类是⼀个抽象概念,那么这些函数就是我们对⼀类对象共同的动态特征的提取。写在类⾥⾯的函数我们通常称之为⽅法
,⽅法就是对象的⾏为,也就是对象可以接收的消息。⽅法的第⼀个参数通常都是 self
,它代表了接收这个消息的对象本身。
class Student:
def study(self, course_name):
print(f'学⽣正在学习{course_name}.')
def play(self):
print(f'学⽣正在玩游戏.')
创建和使⽤对象
在我们定义好⼀个类之后,可以使⽤构造器语法来创建对象,代码如下所示。
stu1 = Student()
stu2 = Student()
print(stu1) # <__main__.Student object at 0x10ad5ac50>
print(stu2) # <__main__.Student object at 0x10ad5acd0>
print(hex(id(stu1)), hex(id(stu2))) # 0x10ad5ac50 0x10ad5acd0
在类的名字后跟上圆括号就是所谓的构造器语法,上⾯的代码创建了两个学⽣对象,⼀个赋值给变量stu1 ,⼀个复制给变量
stu2
。当我们⽤
print
函数打印
stu1
和
stu2
两个变量时,我们会看到输出了对象在内存中的地址(⼗六进制形式),跟我们⽤ id
函数查看对象标识获得的值是相同的。现在我们可以告诉⼤家,我们定义的变量其实保存的是⼀个对象在内存中的逻辑地址(位置),通过这个逻辑地址,我们就可以在内存中找到这个对象。所以 stu3 = stu2
这样的赋值语句并没有创建新的对象,只是⽤⼀个新的变量保存了已有对象的地址。 接下来,我们尝试给对象发消息,即调⽤对象的⽅法。刚才的 Student
类中我们定义了
study
和
play 两个⽅法,两个⽅法的第⼀个参数 self
代表了接收消息的学⽣对象,
study
⽅法的第⼆个参数是学习 的课程名称。Python
中,给对象发消息有两种⽅式,请看下⾯的代码。
# 通过“类.⽅法”调⽤⽅法,第⼀个参数是接收消息的对象,第⼆个参数是学习的课程名称
Student.study(stu1, 'Python程序设计') # 学⽣正在学习Python程序设计.
# 通过“对象.⽅法”调⽤⽅法,点前⾯的对象就是接收消息的对象,只需要传⼊第⼆个参数
stu1.study('Python程序设计') # 学⽣正在学习Python程序设计.
Student.play(stu2) # 学⽣正在玩游戏.
stu2.play() # 学⽣正在玩游戏.
初始化⽅法
⼤家可能已经注意到了,刚才我们创建的学⽣对象只有⾏为没有属性,如果要给学⽣对象定义属性,我们可以修改 Student
类,为其添加⼀个名为
__init__
的⽅法。在我们调⽤
Student
类的构造器创建对象时,⾸先会在内存中获得保存学⽣对象所需的内存空间,然后通过⾃动执⾏ __init__
⽅法,完成对内存的初始化操作,也就是把数据放到内存空间中。所以我们可以通过给 Student
类添加
__init__
⽅法的⽅式为学⽣对象指定属性,同时完成对属性赋初始值的操作,正因如此, __init__
⽅法通常也被称为初始化⽅法。
我们对上⾯的
Student
类稍作修改,给学⽣对象添加
name
(姓名)和
age
(年龄)两个属性。
class Student:
"""学⽣"""
def __init__(self, name, age):
"""初始化⽅法"""
self.name = name
self.age = age
def study(self, course_name):
"""学习"""
print(f'{self.name}正在学习{course_name}.')
def play(self):
"""玩耍"""
print(f'{self.name}正在玩游戏.')
修改刚才创建对象和给对象发消息的代码,重新执⾏⼀次,看看程序的执⾏结果有什么变化。
# 由于初始化⽅法除了self之外还有两个参数
# 所以调⽤Student类的构造器创建对象时要传⼊这两个参数
stu1 = Student('骆昊', 40)
stu2 = Student('王⼤锤', 15)
stu1.study('Python程序设计') # 骆昊正在学习Python程序设计.
stu2.play() # 王⼤锤正在玩游戏.
打印对象
上⾯我们通过
__init__
⽅法在创建对象时为对象绑定了属性并赋予了初始值。在
Python
中,以两个下划线 __
(读作
“dunder”
)开头和结尾的⽅法通常都是有特殊⽤途和意义的⽅法,我们⼀般称之为
魔术
⽅法
或
魔法⽅法
。如果我们在打印对象的时候不希望看到对象的地址⽽是看到我们⾃定义的信息,可以通过在类中放置 __repr__
魔术⽅法来做到,该⽅法返回的字符串就是⽤
print
函数打印对象的时候会显示的内容,代码如下所示。
class Student:
"""学⽣"""
def __init__(self, name, age):
"""初始化⽅法"""
self.name = name
self.age = age
def study(self, course_name):
"""学习"""
print(f'{self.name}正在学习{course_name}.')
def play(self):
"""玩耍"""
print(f'{self.name}正在玩游戏.')
def __repr__(self):
return f'{self.name}: {self.age}'
stu1 = Student('张三', 40)
print(stu1) # 张三: 40
students = [stu1, Student('王⼩锤', 16), Student('王⼤锤', 25)]
print(students) # [张三: 40, 王⼩锤: 16, 王⼤锤: 25]
⾯向对象的⽀柱
⾯向对象编程有三⼤⽀柱,就是我们之前给⼤家划重点的时候圈出的三个词:封装、继承和多态。后⾯两个概念在下⼀节课中会详细说明,这⾥我们先说⼀下什么是封装。我⾃⼰对封装的理解是:隐藏⼀切可以隐藏的实现细节,只向外界暴露简单的调⽤接⼝。我们在类中定义的对象⽅法其实就是⼀种封装,这种封装可以让我们在创建对象之后,只需要给对象发送⼀个消息就可以执⾏⽅法中的代码,也就是说我们在只知道⽅法的名字和参数(⽅法的外部视图),不知道⽅法内部实现细节(⽅法的内部视图)的情况下就完成了对⽅法的使⽤。
举⼀个例⼦,假如要控制⼀个机器⼈帮我倒杯⽔,如果不使⽤⾯向对象编程,不做任何的封装,那么就需要向这个机器⼈发出⼀系列的指令,如站起来、向左转、向前⾛5
步、拿起⾯前的⽔杯、向后转、向前⾛10
步、弯腰、放下⽔杯、按下出⽔按钮、等待
10
秒、松开出⽔按钮、拿起⽔杯、向右转、向前⾛
5步、放下⽔杯等,才能完成这个简单的操作,想想都觉得麻烦。按照⾯向对象编程的思想,我们可以将倒⽔的操作封装到机器⼈的⼀个⽅法中,当需要机器⼈帮我们倒⽔的时候,只需要向机器⼈对象发出倒⽔的消息就可以了,这样做不是更好吗?
在很多场景下,⾯向对象编程其实就是⼀个三步⾛的问题。第⼀步定义类,第⼆步创建对象,第三步给对象发消息。当然,有的时候我们是不需要第⼀步的,因为我们想⽤的类可能已经存在了。之前我们说过,Python
内置的
list
、
set
、
dict
其实都不是函数⽽是类,如果要创建列表、集合、字典对象,我们就不⽤⾃定义类了。当然,有的类并不是Python
标准库中直接提供的,它可能来⾃于第三⽅的代码,如何安装和使⽤三⽅代码在后续课程中会进⾏讨论。在某些特殊的场景中,我们会⽤到名为“
内置对象”
的对象,所谓
“
内置对象
”
就是说上⾯三步⾛的第⼀步和第⼆步都不需要了,因为类已经存在⽽且对象已然创建过了,直接向对象发消息就可以了,这也就是我们常说的“
开箱即⽤
”
。
经典案例
例⼦
1
:定义⼀个类描述数字时钟。
import time
# 定义数字时钟类
class Clock(object):
"""数字时钟"""
def __init__(self, hour=0, minute=0, second=0):
"""初始化⽅法
:param hour: 时
:param minute: 分
:param second: 秒
"""
self.hour = hour
self.min = minute
self.sec = second
def run(self):
"""⾛字"""
self.sec += 1
if self.sec == 60:
self.sec = 0
self.min += 1
if self.min == 60:
self.min = 0
self.hour += 1
if self.hour == 24:
self.hour = 0
def show(self):
"""显示时间"""
return f'{self.hour:0>2d}:{self.min:0>2d}:{self.sec:0>2d}'
# 创建时钟对象
clock = Clock(23, 59, 58)
while True:
# 给时钟对象发消息读取时间
print(clock.show())
# 休眠1秒钟
time.sleep(1)
# 给时钟对象发消息使其⾛字
clock.run()
例⼦
2
:定义⼀个类描述平⾯上的点,要求提供计算到另⼀个点距离的⽅法。
class Point(object):
"""屏⾯上的点"""
def __init__(self, x=0, y=0):
"""初始化⽅法
:param x: 横坐标
:param y: 纵坐标
"""
self.x, self.y = x, y
def distance_to(self, other):
"""计算与另⼀个点的距离
:param other: 另⼀个点
"""
dx = self.x - other.x
dy = self.y - other.y
return (dx * dx + dy * dy) ** 0.5
def __str__(self):
return f'({self.x}, {self.y})'
p1 = Point(3, 5)
p2 = Point(6, 9)
print(p1, p2)
print(p1.distance_to(p2))
简单的总结
⾯向对象编程是⼀种⾮常流⾏的编程范式,除此之外还有
指令式编程
、
函数式编程
等编程范式。由于现实世界是由对象构成的,⽽对象是可以接收消息的实体,所以⾯向对象编程更符合⼈类正常的思维习
惯
。类是抽象的,对象是具体的,有了类就能创建对象,有了对象就可以接收消息,这就是⾯向对象编程的基础。定义类的过程是⼀个抽象的过程,找到对象公共的属性属于数据抽象,找到对象公共的⽅法属于⾏为抽象。抽象的过程是⼀个仁者⻅仁智者⻅智的过程,对同⼀类对象进⾏抽象可能会得到不同的结果,如下图所示
![](https://i-blog.csdnimg.cn/blog_migrate/e45f74a50bea0462f14a331fa391679b.png)
说明:
本节课的插图来⾃于
Grady Booc
等撰写的《⾯向对象分析与设计》⼀书,该书是讲解⾯
向对象编程的经典著作,有兴趣的读者可以购买和阅读这本书来了解更多的⾯向对象的相关知
识。