初识面向对象程序设计
上周六,你在儿童乐园玩了一下午的积木搭城堡游戏,是不是累得满头汗?你先把最大的方形积木块放在地上当底座,再挨个摆上四块长条形的灰色积木当城墙,接着在四个墙角各放一个圆形的红色积木当塔楼,最后把三角形的黄色积木块摞在最中间当屋顶——好不容易搭完,手一碰,城堡哗啦一声塌了,你气得直跺脚。要是每次搭城堡都要重复这些步骤,还要担心它塌掉,是不是太麻烦了?有没有什么办法,能让我们搭城堡又快又稳,还能随便改出自己喜欢的样子呢?其实编程世界里早就有了这样的“魔法工具”,它就是我们要一起学习的面向对象程序设计,还有藏在它背后的设计模式魔法。
<!-- 纸书格式:此处为引言,需居中加粗,字号为小三 --> <!-- 电子版:侧边栏添加小知识提示 --> > 📌 小知识:面向对象程序设计英文是Object-Oriented Programming,简称OOP,是现在最主流的编程思路,几乎所有的大型游戏、APP、网站都是用面向对象思路写出来的。
在编程的世界里,解决问题主要有两种思路:一种是面向过程编程,一种是面向对象编程。刚才你搭城堡的步骤,就是典型的面向过程思路:你把搭城堡拆成一个个独立的步骤,先做A,再做B,最后做C,只要步骤对了,就能搭出城堡。这种思路很简单,适合做很小很简单的任务,比如搭一个只有底座和城墙的小栅栏,但是一旦任务变复杂,比如你要搭一个有3个塔楼、2个城门、1个护城河、还会发光的魔法城堡,面向过程的思路就会变得特别麻烦:你要把搭城堡的步骤写成几十上百行代码,每次要改底座的颜色,就要把所有步骤里的底座颜色都改一遍,漏改一个就会出错;要是想再加一个会飞的屋顶,就要把所有搭屋顶的步骤都改一遍,相当于把之前的代码全重写,太浪费时间了。
而面向对象编程的思路,就像你先画一张城堡的设计图:设计图上标清楚底座是方形、城墙是长条形、塔楼是圆形、屋顶是三角形,还可以写上每个部分的颜色、大小、功能。以后要搭城堡,你只要照着设计图做就行,甚至可以做出来一堆不同颜色、不同大小的城堡,都不用再重新想搭的步骤。这张设计图,在编程里就叫类(Class),而照着设计图做出来的真的积木城堡,就叫对象(Object),也叫实例(Instance)。
你可以把类理解成“饼干模具”,把对象理解成“用模具压出来的饼干”。同一个模具可以压出好多块饼干,每块饼干的基础形状都是一样的,但是你可以给每块饼干加上不同的糖霜、不同的芝麻,让它们变得不一样。类就是那个模具,规定了所有对象的共同特点:比如所有的城堡对象都有底座、城墙、塔楼,都能被搭建、被展示;而每个对象可以有自己的独特属性:比如有的城堡底座是蓝色的,有的是红色的,有的是带爱心的,这些独特的特点就是对象的属性(Attribute),而对象能做的事情,比如搭建、展示、发光,就是对象的方法(Method)。
<!-- 纸书格式:此处为正文小节,三级标题 -->
1.1 面向对象的三大魔法:封装、继承、多态
面向对象有三大核心特性,分别是封装、继承、多态,它们就像面向对象的三大“魔法咒语”,能让我们的编程效率提升好几倍。
1.1.1 封装:把复杂的东西藏起来
第一个魔法是封装(Encapsulation),它的作用是把相关的东西打包在一起,不用让你看到里面的细节。你玩城堡积木套装的时候,所有的积木块都装在一个盒子里,盒子上只留了一个开口让你拿积木,你不用知道里面的塔楼是怎么粘在底座上的,也不用知道城墙是怎么固定在底座上的,只要从开口里拿出积木,照着说明书搭就行。
这个装积木的盒子,就是封装:它把城堡的所有属性(积木的颜色、形状)和方法(怎么搭、怎么拆)都打包在“城堡类”这个单元里,对外只暴露你需要用的接口(盒子的开口),把内部的复杂实现细节全部隐藏起来。封装的魔力可大了:如果没有封装,你每次搭城堡都要自己先把塔楼粘在底座上,再粘城墙,不仅麻烦,还容易粘错位置;有了封装,你只要调用`build()`这个方法,就能直接得到搭好的城堡,不用管内部的拼接逻辑,
就不用自己一个个拼接积木块,只要从盒子里拿出来按顺序摆就行,就算你记不住塔楼是怎么粘在底座上的,也能搭出完整的城堡。更棒的是,如果这个盒子里的拼接逻辑升级了——比如原来的塔楼是用胶粘的,现在改成了卡扣设计,你不用学新的拼接方法,只要还是从盒子里拿积木搭就行,完全不受内部升级的影响,这就是封装最大的魔力:降低复杂度,提高代码的可维护性。
我们可以用一段简单的代码来理解封装的实现,假设我们要写一个城堡类的程序,就可以把城堡的所有属性和方法都打包在`Castle`这个类里面,把内部的城墙高度、拼接逻辑这些不想让外面随便改的东西设为“私有属性”,只能通过类提供的公开方法去修改,就像装积木的盒子只留了一个开口,你只能从开口拿积木,不能直接把手伸进去乱翻里面的零件: ```python
定义城堡类(相当于城堡的设计图)
class Castle: def __init__(self, color, tower_num):
公开属性:颜色、塔楼数量,外面可以直接看和修改
self.color = color self.tower_num = tower_num
私有属性:城墙高度,前面的双下划线表示私有,外面不能直接访问和修改
self.__wall_height = 10
公开方法:搭建城堡,外面可以直接调用
def build(self): print(f"正在搭{self.color}的城堡,有{self.tower_num}个塔楼,城墙高{self.__wall_height}米")
公开方法:修改城墙高度,会先检查高度是不是合理
def set_wall_height(self, height): if height > 20: print("⚠️ 城墙太高会塌!不能超过20米哦") else: self.__wall_height = height print(f"✅ 城墙高度改成{height}米啦")
公开方法:展示城堡
def show(self): print(f"这是一个{self.color}的普通城堡,有{self.tower_num}个塔楼")
实例化(用设计图造实物):创建一个红色、2个塔楼的城堡对象
red_castle = Castle("红色", 2) red_castle.build() # 调用搭建方法 red_castle.set_wall_height(15) # 调用修改高度的方法,会自动检查 red_castle.set_wall_height(25) # 高度太高,会被方法拦截,不会生效 red_castle.show() ``` 你看,有了封装之后,你根本不需要知道城墙高度是怎么存储、怎么检查的,只要调用`set_wall_height`方法输入数值就行,就算输错了,方法也会帮你拦截,不会让城堡搭到一半塌掉,这就是封装的实际作用:把复杂的逻辑藏起来,只给你提供简单好用的接口,同时避免你误操作导致出错。
<!-- 纸书格式:此处为正文小节,三级标题 -->
1.1.2 继承:不用重复造轮子的魔法
讲完了封装这个第一个魔法,我们接下来看第二个魔法:继承(Inheritance)。你有没有过这种经历:你刚搭完一个超级漂亮的普通石头城堡,突然想搭一个魔法城堡,是不是又要重新搭底座、搭城墙、搭塔楼?要是每次搭新城堡都要把之前的步骤重做一遍,是不是特别浪费时间?继承这个魔法,就能让你把之前搭好的城堡的所有部分都“继承”过来,只要在基础上加新的部分就行,不用从头再来。
继承的类比就像你玩积木的「基础套装+扩展包」:你花50块买了一个基础城堡积木套装,里面有底座、城墙、塔楼、普通屋顶,能搭出所有普通的石头城堡;后来你看到出了魔法城堡扩展包,只要20块,里面有魔法水晶、发光屋顶、魔法阵,你只要买了扩展包,就能在普通城堡的基础上加这些新零件,搭出魔法城堡,不用再重新花50块买底座的零件——这个扩展包,就是编程里的继承。
<!-- 电子版:侧边栏小知识 --> > 📌 小知识:继承的“is-a”原则:只有当子类“是”父类的一种的时候,才可以用继承。比如“魔法城堡是城堡”“公主城堡是城堡”,所以可以用继承;但“城堡有积木”是“has-a”关系,不能用继承,要用组合哦。
在编程里,我们已经写好的那个普通`Castle`类就是“基础套装”,如果我们想写魔法城堡的类,不用再把颜色、塔楼数量、搭建方法这些代码重新写一遍,只要让魔法城堡类“继承”城堡类,就能直接拥有父类(被继承的类也叫父类/基类)的所有属性和方法,还能自己加独有的属性和方法,比如魔法等级、发光功能、释放魔法的技能: ```python
魔法城堡类继承城堡类,括号里写要继承的父类名字
class MagicCastle(Castle): def __init__(self, color, tower_num, magic_level):
super()是调用父类的初始化方法,把父类的属性继承过来
super().__init__(color, tower_num)
新增魔法城堡独有的属性:魔法等级
self.magic_level = magic_level self.has_light = True # 新增属性:会发光
重写(Override)父类的show方法:和父类的展示逻辑不一样
def show(self): print(f"这是一个{self.color}的魔法城堡,魔法等级是{self.magic_level}级,晚上会发光哦!")
新增魔法城堡独有的方法:释放魔法
def cast_spell(self): print(f"✨ {self.color}的魔法城堡释放了{self.magic_level}级魔法,变出了漫天的彩虹!")
实例化一个魔法城堡对象:紫色、3个塔楼、魔法等级5
purple_magic_castle = MagicCastle("紫色", 3, 5) purple_magic_castle.build() # 直接调用继承自父类的build方法,不用重新写 purple_magic_castle.show() # 调用自己重写的show方法,输出魔法城堡的专属内容 purple_magic_castle.cast_spell() # 调用自己独有的释放魔法方法 ``` 你看,写魔法城堡类的时候,我们只用了不到10行代码,就实现了所有功能,不用重复写父类已经写好的搭建、属性检查这些逻辑,这就是继承的核心优势:提高代码的复用性,减少重复代码,让代码更简洁。
如果我们要再写一个公主城堡类,只要同样继承`Castle`类,加自己独有的属性比如`has_flower_garden`(有没有花园)、`has_ribbon`(有没有装饰丝带),重写`show`方法输出公主城堡的内容就行,完全不用再写父类的那些基础代码,以后要改底座的大小、城墙的检查逻辑,只要改父类`Castle`的代码,所有继承它的子类(普通城堡、魔法城堡、公主城堡)都会自动生效,不用一个个改,特别方便。
<!-- 纸书格式:此处为正文小节,三级标题 -->
1.
如果我们要再写一个公主城堡类,只要同样继承`Castle`类,加自己独有的属性比如`has_flower_garden`(有没有花园)、`has_ribbon`(有没有装饰丝带),重写`show`方法输出公主城堡的内容就行,完全不用再写父类的那些基础代码,以后要改底座的大小、城墙的检查逻辑,只要改父类`Castle`的代码,所有继承它的子类(普通城堡、魔法城堡、公主城堡)都会自动生效,不用一个个改,特别方便。我们可以把公主城堡的代码写出来验证一下: ```python
公主城堡类,同样继承城堡类
class PrincessCastle(Castle): def __init__(self, color, tower_num, has_garden):
调用父类的初始化方法,继承父类的属性和方法
super().__init__(color, tower_num)
公主城堡独有的属性:有没有玫瑰花园
self.has_flower_garden = has_garden
公主城堡独有的属性:默认有装饰丝带
self.has_ribbon = True
重写父类的show方法,输出公主城堡的专属内容
def show(self): if self.has_flower_garden: print(f"这是一个{self.color}的公主城堡,有{self.tower_num}个塔楼,前面开满了玫瑰花园,还系着粉色丝带~") else: print(f"这是一个{self.color}的公主城堡,有{self.tower_num}个塔楼,装饰着粉色丝带~") ```
<!-- 纸书格式:此处代码需等宽字体印刷,背景为浅灰色 --> <!-- 电子版:代码可一键复制,点击可运行查看效果 -->
你看,写公主城堡类的时候,我们几乎没有写重复的代码,所有基础的搭建、属性检查逻辑都是直接继承父类的,只要加自己独有的属性和方法就行,这就是继承的高效之处。
1.1.3 多态:同一个指令,不同的效果
讲完了封装和继承两个魔法,我们接下来看第三个也是最有意思的魔法:多态(Polymorphism),它的字面意思是“多种形态”,简单来说就是:给不同的对象发同一个指令,不同的对象会做出不同的反应。
还是拿搭城堡举例子:你给所有城堡都发一个“展示自己”的指令,普通城堡会说自己是什么颜色、有多少个塔楼,魔法城堡会说自己会发光、魔法等级是多少,公主城堡会说自己有花园、有丝带——同一个“展示”指令,不同的城堡给出了不同的回应,这就是多态。
在编程里,多态的实现需要两个必要条件:第一,类之间有继承关系;第二,子类重写了父类的方法。我们可以写一段代码来验证:我们创建三个不同的城堡对象(普通城堡、魔法城堡、公主城堡),把它们放到同一个列表里,统一调用`show()`方法,不用管每个对象具体是什么类型,就能得到不同的输出: ```python
创建三个不同的城堡对象
castle_list = [ Castle("灰色", 4), # 普通石头城堡 MagicCastle("紫色", 3, 5), # 紫色魔法城堡,魔法等级5 PrincessCastle("粉色", 2, True) # 粉色公主城堡,带花园 ]
统一调用show方法,不用判断每个对象的类型
print("=== 城堡展示会开始啦 ===") for castle in castle_list: castle.show() ``` 运行这段代码,你会看到三个完全不同的输出结果,完全不需要写三个`if-else`判断对象类型再调用对应的方法,代码特别简洁。这就是多态的核心优势:提高代码的扩展性和灵活性,新增不同类型的对象时,不用修改原有的调用逻辑。
比如以后我们要加一个冰雪城堡类,只要让它继承`Castle`类,重写`show()`方法就行,直接把它加到`castle_list`列表里,不用改循环的任何代码,完全符合设计模式里的“开闭原则”——对扩展开放(可以随便加新的子类),对修改关闭(不用改原有的调用代码)。
1.2 面向过程 vs 面向对象:搭城堡的两种思路
我们前面讲过,面向过程和面向对象是编程的两种核心思路,现在我们用完整的搭城堡例子来对比一下,你会更清楚它们的区别。
1.2.1 面向过程的搭城堡思路
面向过程的思路是“先做什么、再做什么、最后做什么”,把所有任务拆成一个个独立的步骤,按顺序执行就行。用面向过程的思路写搭城堡的代码,是这样的: ```python
面向过程的搭城堡代码
def build_castle(color, tower_num, wall_height):
步骤1:搭底座
print(f"1. 搭{color}的方形底座")
步骤2:搭城墙
print(f"2. 搭{tower_num}面城墙,高{wall_height}米")
步骤3:搭塔楼
print(f"3. 搭{tower_num}个圆形塔楼")
步骤4:搭屋顶
print("4. 搭三角形屋顶") print(f"✅ {color}的城堡搭好啦!")
def show_castle(color, tower_num): print(f"这是一个{color}的普通城堡,有{tower_num}个塔楼")
搭两个不同的城堡,要重复写两次搭建步骤
build_castle("红色", 2, 10) show_castle("红色", 2) build_castle("蓝色", 3, 12) show_castle("蓝色", 3) ``` 这种写法的好处是简单直接,适合非常小的任务,但是问题也很明显:如果要改搭建步骤(比如要加护城河、要加城门),就要把所有的`build_castle`调用都改一遍,漏改一个就会出错;如果要加新的城堡类型(比如魔法城堡),就要写新的搭建函数、新的展示函数,代码会越来越长,越来越难维护。
1.2.2 面向对象的搭城堡思路
而面向对象的思路是“先设计类,再创建对象,让对象自己完成工作”,我们之前写的`Castle`、`MagicCastle`、`PrincessCastle`就是类,创建的对象可以自己调用`build()`、`show()`方法,不用在外面写一堆步骤。如果要搭两个普通城堡、一个魔法城堡,只要这样写: ```python
面向对象的搭城堡代码
创建三个城堡对象
castle1 = Castle("红色", 2) castle2 = Castle("蓝色", 3) magic_castle = MagicCastle("紫色", 3, 5)