Python第六课:从零理解面向对象
文章目录
引言
如果你做过蛋糕,一定知道模具的作用:一按一扣,形状就出来了,不用每个蛋糕都手工捏
Python 里的 类(class) 就是那个模具,对象(object) 就是烤出来的蛋糕
想批量生产结构相似的数据?想给每个数据都配上专属的函数?用面向对象就对了
本文会用最生活化的例子,带你亲手做一个“模具”,再做出几个“蛋糕”,并在这个过程中彻底搞懂 Python 的面向对象基础
在开始之前先检查一下你的装备吧!!!
python环境不会装的看这里:从安装到Hello World:Python环境搭建完整指南
python编辑器不会装的看这里:零基础Python入门:手把手教你安装Python、新版PyCharm和VS Code
为什么需要面向对象?
先看一个小需求:
你的程序里要记录两个学生的姓名和语文、数学成绩,并且能打印每个学生的总分,以及判断是否及格
面向过程的写法
你可能会这样写:
# 用两个字典存学生信息 student1 ={'name':'小明','chinese':85,'math':92} student2 ={'name':'小红','chinese':78,'math':88}# 写一个函数计算总分deftotal_score(student):return student['chinese']+ student['math']# 写一个函数判断是否及格(两科均≥60)defis_pass(student):return student['chinese']>=60and student['math']>=60# 使用print(student1['name'], total_score(student1), is_pass(student1))print(student2['name'], total_score(student2), is_pass(student2))这段代码有什么问题?
- 数据和操作是分离的
字典只存数据,函数在外面操作字典。如果以后要增加一个“英语”字段,所有函数都得改,还得小心别漏掉 - 代码散乱
学生一多,字典定义散落在各处,函数也越堆越多,维护起来像在翻垃圾堆 - 没有约束
调用total_score时,你传一个完全不相关的字典进去,比如{'a':1, 'b':2},程序不会报错,但结果毫无意义——这不是“学生”,只是恰好有三个键 - 重复代码
如果想把学生信息传给另一个模块,你得把所有相关函数一起拷过去,否则对方没法操作
用面向对象的写法
把“学生”这个概念抽象成一个类,数据和操作都装在里面:
classStudent:def__init__(self, name, chinese, math): self.name = name self.chinese = chinese self.math = math deftotal_score(self):return self.chinese + self.math defis_pass(self):return self.chinese >=60and self.math >=60# 创建对象 s1 = Student('小明',85,92) s2 = Student('小红',78,88)# 使用print(s1.name, s1.total_score(), s1.is_pass())print(s2.name, s2.total_score(), s2.is_pass())这个版本解决了什么问题?
- 数据和操作在一起
总分、及格判断都长在学生这个类里面,逻辑上就是一个整体 - 扩展方便
要加“英语”成绩,只需要在__init__里加一个参数,然后把total_score和is_pass改一下——所有用到Student的地方自动生效 - 代码即文档
Student类本身就告诉了你:一个学生必须有姓名、语文、数学成绩,并且可以算总分、判及格。意图清晰 - 复用简单
想把学生管理功能拿到另一个项目,直接把整个Student类复制过去就行,不用东拼西凑
📌 这就是面向对象的核心思想
把相关数据和操作打包成一个整体(类),然后用这个整体去创建具体个体(对象)
你不再关心“我该怎么操作这些散乱的字典”,而是问“这个对象能帮我做什么”。代码的组织方式从 “函数处理数据” 变成了 “对象提供服务”
听起来有点抽象?没关系,接下来我们就一步步从零开始,教你亲手写出这样的类。你会发现,它比你想的要简单得多
什么是面向对象?
1. 先聊聊 面向过程
在彻底搞懂面向对象之前,我们需要先认识一下它的“前任”:面向过程编程
🔧 面向过程:按步骤办事
面向过程的核心思想是:把一件事拆成若干个步骤,然后用函数把每个步骤封装起来,按顺序调用
举个最生活的例子:做番茄炒蛋
面向过程的思路是这样:
- 准备食材(番茄、鸡蛋、盐)
- 打蛋、切番茄
- 热锅、倒油
- 炒蛋、盛出
- 炒番茄、加蛋、加盐
- 装盘
写成代码就是:
defprepare():print('准备食材')defcut():print('切番茄、打鸡蛋')deffry():print('先炒蛋,再炒番茄,混合调味')defserve():print('装盘上桌') prepare() cut() fry() serve()特点:
- 关注的是 “做什么” 和 “怎么做”
- 程序 = 数据结构 + 算法
- 代码以函数为单位组织,数据在函数间传来传去
2. 面向对象
面向对象换了个角度:我不关心步骤,我关心 “谁来做”
同样做番茄炒蛋,面向对象会这样想:
- 有一个 厨师 角色
- 厨师有自己的技能(切菜、炒菜、调味)
- 厨师使用 食材(番茄、鸡蛋、调味料)
- 最后生产出 菜肴(番茄炒蛋)
写成代码就是:
classChef:defcut(self, food):print(f'切{food}')deffry(self,*ingredients):print(f'炒{"、".join(ingredients)}')defserve(self, dish_name):print(f'{dish_name}装盘')classIngredient:def__init__(self, name): self.name = name # 创建对象 chef = Chef() tomato = Ingredient('番茄') egg = Ingredient('鸡蛋') salt = Ingredient('盐')# 厨师干活 chef.cut(tomato.name) chef.cut(egg.name) chef.fry(tomato.name, egg.name, salt.name) chef.serve('番茄炒蛋')特点:
- 关注的是 “谁有责任做这件事”
- 程序 = 一堆对象 + 对象之间的交互
- 数据和操作它的方法打包在同一个类里
3. 面向过程 vs 面向对象
我也看到过一种解释方法说:面向过程是编年体; 面向对象是纪传体
| 对比维度 | 面向过程 | 面向对象 |
|---|---|---|
| 核心思想 | 按步骤解决问题 | 按角色分配责任 |
| 组织单位 | 函数 | 类 / 对象 |
| 数据与操作 | 分离(数据在函数间传递) | 聚合(数据和操作封装在一起) |
| 核心概念 | 函数、变量 | 类、对象、属性、方法 |
| 代码复用 | 函数复用 | 类复用(继承、组合) |
| 适用场景 | 小型、简单、线性流程(如脚本、工具函数) | 大型、复杂、多人协作(如Web后端、游戏、GUI) |
| 优点 | 简单直接,性能开销小 | 易维护、易扩展、更贴近现实世界 |
| 缺点 | 代码一长就混乱,难以维护 | 学习曲线稍陡,设计成本较高 |
4. 举个例子
🌰 再举个对比:学生成绩管理
面向过程风格:
students =[{'name':'小明','score':85},{'name':'小红','score':92}]defadd_student(students, name, score): students.append({'name': name,'score': score})defavg_score(students):returnsum(s['score']for s in students)/len(students) add_student(students,'小刚',78)print(avg_score(students))面向对象风格:
classStudent:def__init__(self, name, score): self.name = name self.score = score classClass:def__init__(self): self.students =[]defadd(self, student): self.students.append(student)defaverage(self):returnsum(s.score for s in self.students)/len(self.students) cls = Class() cls.add(Student('小明',85)) cls.add(Student('小红',92)) cls.add(Student('小刚',78))print(cls.average())对比之下,面向对象的版本把“学生”和“班级”都变成了有明确职责的对象,代码的可读性和扩展性明显更好
5. 小结
🎯 那是不是面向对象就一定更好?
那肯定不是
- 如果你只是在写几十行的小脚本,用面向过程完全没问题,更直接、更快
- 一旦程序规模变大、需求频繁变化、或者需要多人协作,面向对象的优势就会体现出来——更容易扩展、更容易测试、更容易让不同的人分工开发不同类
现代开发通常是混合使用:顶层用面向对象组织架构,底层某些复杂算法仍然用面向过程的函数来实现
- 面向过程:以“步骤”为中心,适合线性、简单的任务
- 面向对象:以“角色”为中心,适合复杂、长期维护的系统
- 两者不是对立关系,而是不同层次的工具。学会面向对象,不是要抛弃面向过程,而是给你的工具箱里多添一把利器
类与对象:模具与产品
面向对象里最核心的两个概念就是 类(Class) 和 对象(Object)
很多人一开始会被这两个词绕晕,其实用一个生活比喻就能立刻搞懂
🍰 类比:蛋糕模具与蛋糕
想象一下你开了一家甜品店,想批量生产蛋糕
- 你不可能每次烤蛋糕都从零捏形状——太慢,而且每个蛋糕形状都不一样
- 你会先做一个模具:这个模具固定了蛋糕的大小、形状、厚度
- 然后你用这个模具,倒面糊、进烤箱,做出一个个蛋糕
在这个场景里:
- 模具 → 类:它是抽象的模板,定义了产品长什么样
- 蛋糕 → 对象:它是具体的产品,按照模具生产出来的实物
模具只有一个,蛋糕可以有无数个
类在代码里只定义一次,但你可以用它创建任意多个对象
1. 创建你的第一个类
我们用一个最简单的例子:Dog 类
classDog:pass就这么简单,一个类定义好了
class是定义类的关键字Dog是类名,建议首字母大写,采用大驼峰命名法(每个单词首字母大写,如BigDog,GoldenRetriever)pass是占位符,表示这个类暂时什么都不做- 注:类名后面紧跟
:,占位符前面要有4个空格(按一下TAB键,效果一样)
2. 用类创建对象
模具做好之后,就可以生产蛋糕了:
my_dog = Dog()# 创建第一个对象 your_dog = Dog()# 创建第二个对象my_dog和your_dog都是Dog类的对象(也叫实例)- 每调用一次
Dog(),Python 就会在内存里新分配一块空间,给你一个全新的、独立的对象
验证一下:
print(my_dog)print(your_dog)输出(你的内存地址可能不同):
看到了吗?两个不同的内存地址,说明是两个不同的对象
3. 类与对象的本质区别
| 类(Class) | 对象(Object) | |
|---|---|---|
| 角色 | 模具 | 产品 |
| 数量 | 1个 | 无数个 |
| 本质 | 代码里的定义 | 内存里的实体 |
| 是否占内存 | 不占(加载时占用极小代码段) | 每个对象都占独立内存 |
| 能否直接操作 | 不能直接用来干活 | 能调用方法、访问属性 |
一句话总结:类是创建对象的蓝图,对象是类的具体表现
🧪 试试看
在你的编辑器里运行这段完整代码:
classDog:pass dog1 = Dog() dog2 = Dog()print(dog1)print(dog2)print(type(dog1))# <class '__main__.Dog'>你会看到两个不同地址的对象,它们的类型都是 Dog
4. 小结
📌 本节核心
- 类是抽象模板,对象是具体实例
- 用
class 类名:定义类,类名首字母大写 - 用
类名()创建对象,每调用一次生成一个新对象 - 类是代码层面的定义,对象是运行时在内存里的实体
现在的 Dog 类还是个空壳子,什么属性和方法都没有。下一节我们给它装上属性(名字、年龄),再配上动作(叫、跑),让它真正“活”起来
💬 小思考
如果我用 Dog() 创建了 100 个对象,内存里会同时存在几个 Dog 类?
(答案:1 个类定义,100 个对象实例)
给类添加属性
上一节我们做的 Dog 类还是个空模具,只能看出“这是一个狗类”,但每条狗叫什么名字、多大年纪、什么品种——这些具体的数据都存不进去
没有数据的对象,就像一个没装蛋糕的模具,只是个空壳
这一节我们就来给类装上属性,让每个对象都有自己的“身份信息”
🏷️ 什么是属性?
属性就是对象身上的数据
比如一条狗,它有名字、年龄、毛色;一辆车,它有品牌、颜色、排量
在 Python 里,属性就是依附在对象上的变量
🛠️ 第一种方式:直接给对象挂属性(不推荐)
你可以在创建对象之后,像挂钥匙一样直接给它加上属性:
classDog:pass my_dog = Dog() my_dog.name ='旺财'# 给对象挂一个 name 属性 my_dog.age =2# 再挂一个 age 属性print(my_dog.name)# 旺财print(my_dog.age)# 2这确实能用,但非常不推荐,因为:
- 每只狗都得手动一条条挂属性,代码重复
- 如果忘记给某只狗挂
name,后面调用print(dog.name)就直接报错 - 同一个类的不同对象,属性可能不一致(有的狗有 age,有的没有)
这种方式的唯一用途是临时调试,正式写代码请跳过它
✅ 正确方式:用 init 方法定义实例属性
Python 的类里有一个特殊方法叫 __init__(前后各两个下划线),在创建对象时会自动执行
我们把初始化属性的代码写在这里,以后每创建一个对象,__init__ 就会被调用一次,自动给新对象装上属性
classDog:def__init__(self, name, age): self.name = name # 给当前对象添加 name 属性 self.age = age # 给当前对象添加 age 属性用这个类创建对象:
dog1 = Dog('旺财',2) dog2 = Dog('来福',3)print(dog1.name, dog1.age)# 旺财 2print(dog2.name, dog2.age)# 来福 3发生了什么?
Dog('旺财', 2)执行时,Python 自动调用__init__方法self就是刚刚被创建出来的那个对象(比如dog1)self.name = name表示把参数'旺财'存进dog1对象里,作为它的name属性self.age = age同理
为什么叫 self?
你可以理解成“我自己”。谁调用这个方法,self 就是谁
这是约定俗称的写法,强烈建议不要改成其他名字
🧪 完整可运行示例
classDog:def__init__(self, name, age): self.name = name self.age = age # 创建两只狗 dog1 = Dog('旺财',2) dog2 = Dog('来福',3)# 访问属性print(f'{dog1.name} 今年 {dog1.age} 岁')print(f'{dog2.name} 今年 {dog2.age} 岁')输出:

🧬 实例属性 vs 类属性
除了每个对象独有的实例属性,还有一种类属性——它是属于整个类的,所有对象共享
定义方式: 直接写在类内部,不在任何方法里
classDog:# 类属性 species ='哺乳动物'def__init__(self, name, age): self.name = name # 实例属性 self.age = age # 实例属性使用:
dog1 = Dog('旺财',2) dog2 = Dog('来福',3)print(dog1.name)# 旺财(实例属性)print(dog2.name)# 来福(实例属性)print(dog1.species)# 哺乳动物(类属性)print(dog2.species)# 哺乳动物(类属性)print(Dog.species)# 哺乳动物(通过类名直接访问)内存图理解:
- 实例属性:每个对象单独保存一份,互不影响
- 类属性:只在类里保存一份,所有对象都指向同一个
🔍 实例属性 vs 类属性:对比表格
| 对比项 | 实例属性 | 类属性 |
|---|---|---|
| 定义位置 | __init__ 或其他实例方法中,用 self.属性名 | 直接在类内部,方法外部 |
| 归属 | 属于具体对象 | 属于类本身 |
| 存储 | 每个对象独立存储一份 | 类内存空间只存一份 |
| 访问方式 | 必须通过对象访问 | 可通过对象或类名访问 |
| 修改影响 | 只影响当前对象 | 通过类名修改,影响所有对象 |
| 典型用途 | 描述对象特有的数据(姓名、年龄) | 描述类别共有特征(物种、常量) |
⚠️ 一个常见的坑
类属性可以通过对象访问,但如果你通过对象给类属性赋值,并不会修改类属性,而是给这个对象新建了一个同名的实例属性
classDog: species ='哺乳动物' dog1 = Dog() dog2 = Dog() dog1.species ='鸟类'# 这里!不是修改类属性,而是给 dog1 创建了实例属性print(dog1.species)# 鸟类(实例属性)print(dog2.species)# 哺乳动物(类属性,没变)print(Dog.species)# 哺乳动物(类属性,确实没变)结论:
- 修改类属性请用
类名.属性 = 新值 - 不要通过对象给类属性赋值——那通常不是你想要的效果
小结
- 实例属性:通过
self.属性名 = 值在__init__中定义,每个对象独立拥有 - 类属性:直接在类中定义,所有对象共享一份
__init__方法在创建对象时自动执行,是最常用的属性初始化场所self是对象自身,是连接方法内部和对象属性的桥梁
💬 小思考
如果我在 __init__ 里写 self.name = name,又在类的最上面写 name = '狗'(类属性),然后创建对象时传入 '旺财',那么对象的 name 是多少?类属性 name 会被覆盖吗?
(答案:对象的name是'旺财';类属性name依然存在且值为'狗',两者互不干扰。)
给类添加方法
有了属性的对象,就像有了身份证,能知道“它是谁、几岁了”
但一条狗不能只是躺着——它得会叫、会跑、会吃饭,对吧?
这些“行为”在面向对象里就叫方法(Method)
方法是定义在类里的函数,专门为这个类的对象服务
🎬 什么是方法?
简单说:写在类里面的函数,就叫方法
但方法和普通函数有一个关键区别:方法必须有一个参数self,代表调用这个方法的对象本身
看个最简版本:
classDog:def__init__(self, name): self.name = name defbark(self):# 这是一个方法print('汪汪汪!')bark是一个方法,它没有接收外部参数,只有self- 当调用
dog1.bark()时,Python 会自动把dog1传给self,所以你调用时不用传 self
📞 调用方法
classDog:def__init__(self, name): self.name = name defbark(self):print(f'{self.name}:汪汪汪!') my_dog = Dog('旺财') my_dog.bark()# 旺财:汪汪汪!过程解析:
my_dog.bark()→ Python 翻译成Dog.bark(my_dog)- 方法内部的
self.name就是my_dog.name,也就是'旺财' - 打印出
旺财:汪汪汪!
这就是 self 的魔法:它让方法知道自己是为哪个对象服务的
🎯 方法可以访问和修改实例属性
方法内部不仅可以读属性,还可以改属性:
classDog:def__init__(self, name, age): self.name = name self.age = age self.hungry =True# 刚出生都饿着defeat(self): self.hungry =Falseprint(f'{self.name} 吃饱了')defbark(self):print(f'{self.name} 叫了一声') dog = Dog('旺财',2)print(dog.hungry)# True dog.eat()# 旺财 吃饱了print(dog.hungry)# Falseeat 方法修改了 hungry 属性,对象的状态发生了变化
🧮 方法也可以有自己的参数
除了 self,方法可以像普通函数一样接受额外参数:
classDog:def__init__(self, name): self.name = name defbark(self, times):for _ inrange(times):print(f'{self.name}:汪!') dog = Dog('来福') dog.bark(3)# 叫三声输出:
🧠 方法 vs 函数:一张表看懂
| 对比点 | 普通函数 | 实例方法 |
|---|---|---|
| 定义位置 | 模块中 | 类内部 |
| 第一个参数 | 任意 | 必须是 self(约定) |
| 调用方式 | 函数名(参数) | 对象.方法名(参数) |
| 能否直接访问对象属性 | 不能(需传对象) | 能(通过 self) |
| 归属 | 属于模块 | 属于类/对象 |
🧪 完整示例:让狗跑起来
classDog:def__init__(self, name, speed=5): self.name = name self.speed = speed self.position =0defrun(self, seconds): distance = self.speed * seconds self.position += distance print(f'{self.name} 跑了 {distance} 米,现在位置在 {self.position} 米处')defbark(self):print(f'{self.name}:汪汪!我在位置 {self.position}') dog = Dog('闪电',8) dog.run(3)# 闪电 跑了 24 米,现在位置在 24 米处 dog.bark()# 闪电:汪汪!我在位置 24 dog.run(2)# 闪电 跑了 16 米,现在位置在 40 米处这个例子展示了:
- 实例属性(
name,speed,position) - 实例方法(
run,bark) - 方法内部操作属性(读取、修改)