第7章 7.1 class 与 init:面向对象的第一步
全文约 5000 字,阅读约 90 分钟
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了用 print、pdb、logging 三招来调试代码,终于能看懂程序为什么会跑偏了。但调试只是"事后补救",你有没有想过:如果一开始就把代码组织得更好,是不是就不需要天天调试了?
想象一下这个场景:你是一个班主任,要管理 50 个学生的成绩。每次考试结束后,你要录入成绩、算平均分、找最高分、找最低分……如果你用变量来做,大概会写成这样:
student1_name = "张三"
student1_score = 85
student2_name = "李四"
student2_score = 92
# ... 写到第50个学生
写到第 10 个的时候你就疯了——变量名根本记不住,函数传参传一堆,而且如果要让程序「找出平均分最高的同学」,你得写一大段循环和条件判断。
学完这一章,你能解决这些问题:
- 如何用「装东西的盒子」把一个学生的姓名、成绩、班级等信息打包管理?
- 如何批量创建 50 个学生对象,而不是手动写 50 遍变量?
- 如何让「计算平均分」「找最高分」这些操作变成学生对象自带的功能?
说的更直白一点:这一章教你写代码的「收纳术」,让你从「乱堆变量」进化到「有序封装」。
🧱 基础 25 分钟:核心概念
7.1.1 class 是什么?——"模具"和"产品"的关系
是什么(生活类比):
想象你开了一家模具工厂。你设计了一个「学生模具」,这个模具规定了:每个学生必须有姓名(name)、年龄(age)、成绩(score)这三个属性。
模具本身不是具体的学生,但你可以用这个模具「浇筑」出无数个真实的学生——张三、李四、王五……
在 Python 里,class 就是那个「模具」。
为什么要用(解决啥痛点):
- 不用 class:50 个学生 → 50 组零散的变量 → 没法批量操作
- 用 class:1 个「学生模具」→ 50 个「学生对象」→ 一行代码就能让所有学生报数
怎么用(最简代码):
# 定义一个「学生模具」
class Student:
pass # 先空着,下一小节再填内容
# 用模具创建一个具体的学生对象
xiaoming = Student() # 小明是真实的学生了!
print(xiaoming) # <__main__.Student object at 0x7f...>
代码解释:
- class Student: 定义了一个叫 Student 的模具
- pass 表示模具里暂时啥都没有,先占个位置
- Student() 是「用模具浇筑出一个产品」的动作
- xiaoming 就是浇筑出来的具体产品(对象)

7.1.2 init 是什么?——"新生报到登记处"
是什么(生活类比):
新生入学第一天要去「报到处」登记:填写姓名、录入学费、分配班级……这个报到登记的流程,每个新生都得走一遍。
在 Python 的 class 里,__init__ 就是那个「报到处」——每个对象被创建时,必须先经过这里,填入自己的基本信息。
为什么要用(解决啥痛点):
如果你创建的对象都是空的,创建出来有啥用?__init__ 让你在创建对象的一刻,就把它的"身份信息"固定好。
怎么用(最简代码):
class Student:
def __init__(self, name, age, score):
# self 是什么后面讲,先看这三行
self.name = name # 把传进来的 name 存到这个学生的 name 属性里
self.age = age # 把传进来的 age 存到这个学生的 age 属性里
self.score = score # 把传进来的 score 存到这个学生的 score 属性里
# 创建小明
xiaoming = Student("小明", 15, 88)
# 验证小明的属性
print(f"姓名:{xiaoming.name}") # 姓名:小明
print(f"年龄:{xiaoming.age}") # 年龄:15
print(f"成绩:{xiaoming.score}") # 成绩:88
代码解释:
- def __init__(self, name, age, score): 定义了报到处的"登记表格"
- self.name = name 意思是「把这个人的名字,写入这个人的档案里」
- 创建对象时 Student("小明", 15, 88) 就是在"报到",把信息交给 __init__
注意!__init__ 的两个下划线不能省略,这是 Python 的「魔法方法」——特殊时刻会被自动调用。
7.1.3 self 是什么?——"我"是谁?
是什么(生活类比):
想象你对自己说:「我要把我的名字改成小红」。这个「我」指的是谁?指的是你自己,不是别人。
在 class 里的 self 就是这个意思——指向"当前正在操作的这个具体对象"。
为什么要用(解决啥痛点):
假设教室里同时有张三和李四两个学生。当张三说「我要加分」的时候,程序得知道是给张三加,不是给李四加。self 就是用来区分「哪个对象在说话」的关键。
怎么用(最简代码):
class Student:
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
def introduce(self): # 定义一个"自我介绍"的方法
# self 在这里代表「调用这个方法的本人」
print(f"大家好,我叫{self.name},今年{self.age}岁,成绩是{self.score}分")
# 创建两个学生
xiaoming = Student("小明", 15, 88)
xiaohong = Student("小红", 14, 95)
# 调用方法时,self 会自动指向调用者
xiaoming.introduce() # 输出:大家好,我叫小明,今年15岁,成绩是88分
xiaohong.introduce() # 输出:大家好,我叫小红,今年14岁,成绩是95分
代码解释:
- def introduce(self): 定义方法时,第一个参数必须是 self
- 调用 xiaoming.introduce() 时,Python 自动把 xiaoming 传给了 self
- 所以 self.name 在小明的场合就是 "小明",在小红的场合就是 "小红"

7.1.4 实例属性 vs 类属性——"我的"和"大家的"
是什么(生活类比):
- 实例属性:小明的姓名、小红的年龄——这些是「每个人都有自己的一份」的东西
- 类属性:学校名称、校长姓名——这些是「全校共享一份」的东西
为什么要用(解决啥痛点):
如果把学校名称存成每个学生的实例属性,那改一次学校名要改 50 个地方。类属性只需要改一个地方,全校生效。
怎么用(最简代码):
class Student:
school_name = "第一中学" # 类属性:全校共享,写在 class 里面
def __init__(self, name, score):
self.name = name # 实例属性:每个人有自己的
self.score = score # 实例属性:每个人有自己的
# 创建两个学生
xiaoming = Student("小明", 88)
xiaohong = Student("小红", 95)
# 访问实例属性(通过对象)
print(xiaoming.name) # 小明
# 访问类属性(通过类或对象都行)
print(Student.school_name) # 第一中学
print(xiaoming.school_name) # 第一中学(也能通过对象访问,但不推荐)
# 修改类属性(通过类)
Student.school_name = "实验中学"
print(xiaoming.school_name) # 实验中学(所有对象都变了)
print(xiaohong.school_name) # 实验中学
代码解释:
- school_name = "第一中学" 写在 class 里的变量,叫「类属性」
- self.name = name 在 __init__ 里写的,叫「实例属性」
- 修改类属性影响所有对象,修改实例属性只影响那一个对象
7.1.5 实例方法 —— 对象能做什么
是什么(生活类比):
学生能做什么?考试、举手发言、做作业……这些是学生能执行的动作,在 class 里叫「实例方法」。
为什么要用(解决啥痛点):
把「操作数据」的代码,写成「对象的方法」,比写成外部函数更直观。student.compute_average() 一读就知道是「这个学生去算平均分」。
怎么用(最简代码):
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
# 根据分数返回等级
if self.score >= 90:
return "A"
elif self.score >= 80:
return "B"
elif self.score >= 60:
return "C"
else:
return "D"
xiaoming = Student("小明", 85)
print(xiaoming.get_grade()) # B
代码解释:
- def get_grade(self): 定义在 class 里的函数,叫「实例方法」
- 方法内部可以用 self.属性名 访问这个对象的属性
- 调用时 xiaoming.get_grade() 不需要传 self 参数,Python 自动传
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):学生成绩管理器
需求:创建 3 个学生,打印每个人的等级
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 90:
return "A"
elif self.score >= 80:
return "B"
elif self.score >= 60:
return "C"
else:
return "D"
# 创建3个学生
students = [
Student("小明", 85),
Student("小红", 92),
Student("小刚", 58)
]
# 打印每个人
for s in students:
print(f"{s.name}的成绩是{s.score}分,等级{s.get_grade()}")
预期输出:
小明的成绩是85分,等级B
小红的成绩是92分,等级A
小刚的成绩是58分,等级D
一句话解释:用 class 把「学生数据 + 能做的事」打包成对象,循环处理时逻辑更清晰。
项目 2(15 分钟):从 CSV 批量导入学生数据
需求:有一份 students.csv 文件,内容如下,批量导入并计算班级平均分
name,age,score
小明,15,85
小红,14,92
小刚,16,78
小美,15,88
小强,14,95
完整可运行代码:
import csv # Python内置处理CSV文件的库
class Student:
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
def get_grade(self):
if self.score >= 90:
return "A"
elif self.score >= 80:
return "B"
elif self.score >= 60:
return "C"
else:
return "D"
# 从CSV读取数据
students = []
with open("students.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f) # 读取成字典格式
for row in reader:
# 关键:把字符串转成整数!
s = Student(row["name"], int(row["age"]), int(row["score"]))
students.append(s)
# 打印所有人
print("=== 班级成绩单 ===")
for s in students:
print(f"{s.name} | 年龄{s.age} | 成绩{s.score} | 等级{s.get_grade()}")
# 计算平均分
average = sum(s.score for s in students) / len(students)
print(f"\n班级平均分:{average:.1f}分")
# 找最高分和最低分
top_student = max(students, key=lambda s: s.score)
low_student = min(students, key=lambda s: s.score)
print(f"最高分:{top_student.name},{top_student.score}分")
print(f"最低分:{low_student.name},{low_student.score}分")
预期输出:
=== 班级成绩单 ===
小明 | 年龄15 | 成绩85 | 等级B
小红 | 年龄14 | 成绩92 | 等级A
小刚 | 年龄16 | 成绩78 | 等级C
小美 | 年龄15 | 成绩88 | 等级B
小强 | 年龄14 | 成绩95 | 等级A
班级平均分:87.6分
最高分:小强,95分
最低分:小刚,78分
一句话解释:csv.DictReader 帮你按行读文件,int() 把 CSV 字符串转成数字,不然 85 + 92 会变成 "8592" 而不是 177。
⚠️ 注意! 如果你忘记把 score 转成 int,Python 会报错:TypeError: '<' not supported between instances of 'str' and 'int'。因为字符串 "85" 和 92 比大小是不行的。
项目 3(15 分钟):待办事项管理器(命令行版)
需求:做一个命令行待办清单,可以添加、完成、查看列表,数据存在内存里(重启程序不保存,简单版)
完整可运行代码:
class TodoItem:
"""单个待办事项"""
def __init__(self, title, description=""):
self.title = title
self.description = description
self.done = False # 新事项默认未完成
def mark_done(self):
self.done = True
def __str__(self):
status = "✅" if self.done else "⬜"
return f"{status} {self.title}"
class TodoList:
"""待办清单管理器"""
def __init__(self, name):
self.name = name
self.items = []
def add(self, title, description=""):
item = TodoItem(title, description)
self.items.append(item)
print(f"已添加:「{title}」")
def complete(self, index):
# index 从 1 开始显示,但列表从 0 开始
if 0 <= index - 1 < len(self.items):
self.items[index - 1].mark_done()
print(f"已完成:「{self.items[index - 1].title}」")
else:
print(f"错误:序号 {index} 不存在")
def show_all(self):
print(f"\n=== {self.name} ===")
if not self.items:
print("(空清单)")
return
for i, item in enumerate(self.items, 1):
print(f"{i}. {item}")
done_count = sum(1 for item in self.items if item.done)
print(f"进度:{done_count}/{len(self.items)} 已完成")
# 演示用法
if __name__ == "__main__":
my_todos = TodoList("我的待办")
# 添加事项
my_todos.add("写完第7章教程", "面向对象入门")
my_todos.add("复习 class 和 __init__")
my_todos.add("做练习题")
# 查看列表
my_todos.show_all()
# 完成第二项
print("\n-- 完成第2项 --")
my_todos.complete(2)
my_todos.show_all()
预期输出:
已添加:「写完第7章教程」
已添加:「复习 class 和 __init__」
已添加:「做练习题」
=== 我的待办 ===
1. ⬜ 写完第7章教程
2. ⬜ 复习 class 和 __init__
3. ⬜ 做练习题
进度:0/3 已完成
-- 完成第2项 --
已完成:「复习 class 和 __init__」
=== 我的待办 ===
1. ⬜ 写完第7章教程
2. ✅ 复习 class 和 __init__
3. ⬜ 做练习题
进度:1/3 已完成
一句话解释:用两个 class 分工——TodoItem 管「单个事项的数据」,TodoList 管「整体操作」,职责分离更清晰。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:忘记 self,方法变成"裸函数"
# ❌ 错误示例
class Student:
def __init__(self, name):
name = name # 以为在存属性,其实只是创建了局部变量
# ✅ 正确示例
class Student:
def __init__(self, name):
self.name = name # 加上 self. 才是存到对象身上
解释:name = name 两条 name 都是局部变量,跟 self 没半毛钱关系。
坑 2:类属性被当成实例属性
# ❌ 错误示例
class Student:
school = "第一中学" # 这是类属性
def __init__(self, name):
self.name = name # 只初始化了 name
xiaoming = Student("小明")
xiaohong = Student("小红")
# 想给小明换学校
xiaoming.school = "实验中学" # 这不是修改类属性,而是给小明创建了一个新的实例属性!
print(xiaoming.school) # 实验中学(只影响小明)
print(xiaohong.school) # 第一中学(不受影响,因为改的是小明的实例属性,不是类属性)
print(Student.school) # 第一中学(类本身没变)
# ✅ 正确示例:理解你要改的是实例还是类
# 如果要改类属性(全校改名):
Student.school = "实验中学" # 通过类来改,影响所有对象
# 如果要改单个学生(只是备注一下这个学生转学了):
xiaoming.school_name = "实验中学" # 给这个对象创建新实例属性,不影响其他对象
坑 3:可变默认参数(list/dict 作为默认参数)
# ❌ 错误示例
class Student:
def __init__(self, name, scores=[]): # 危险!默认参数在函数定义时创建一次
self.name = name
self.scores = scores
def add_score(self, score):
self.scores.append(score)
s1 = Student("小明")
s1.add_score(85)
s2 = Student("小红")
print(s2.scores) # [85] ← 惊不惊喜?小红没加过分,但列表里有85!
# ✅ 正确示例
class Student:
def __init__(self, name, scores=None):
self.name = name
self.scores = scores if scores is not None else [] # 每次创建新列表
def add_score(self, score):
self.scores.append(score)
解释:Python 的默认参数只在函数定义时创建一次,不是在每次调用时创建。所以 [] 被所有调用共享了。
坑 4:打印对象看不到有用信息
# ❌ 调试噩梦
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
s = Student("小明", 85)
print(s) # <__main__.Student object at 0x7f...> ← 这串地址对调试毫无帮助
# ✅ 正确示例:加上 __str__ 方法
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def __str__(self):
return f"Student(name={self.name}, score={self.score})"
s = Student("小明", 85)
print(s) # Student(name=小明, score=85)
坑 5:比较对象时用 == 比较地址
# ❌ 可能踩坑
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
s1 = Student("小明", 85)
s2 = Student("小明", 85)
print(s1 == s2) # False ← 两个"长得一样"的学生,比较结果是"不一样"!
# ✅ 理解原因:默认 == 比较的是"对象身份"(内存地址),不是"内容"
# 如果需要比较内容,需要自定义 __eq__ 方法(进阶内容,这里先知道有这么回事)
print(s1 is s2) # False,is 比较的是身份
print(s1.name == s2.name) # True,比较属性是可以的
性能小贴士:批量创建时用列表推导式
# 普通写法(慢,不推荐)
students = []
for i in range(1000):
s = Student(f"学生{i}", 80)
students.append(s)
# 推荐写法(一行搞定)
students = [Student(f"学生{i}", 80) for i in range(1000)]
调试技巧:用 __dict__ 查看对象的所有属性
class Student:
school = "第一中学" # 类属性
def __init__(self, name, score):
self.name = name # 实例属性
self.score = score
xiaoming = Student("小明", 85)
# 查看这个对象身上有哪些属性
print(xiaoming.__dict__)
# {'name': '小明', 'score': 85}
# 查看类的所有属性(包括继承的)
print(Student.__dict__)
__dict__ 是 Python 对象的"内部字典",调试时用它看看对象到底有什么属性,非常好用。
✏️ 练习题
练习 1(2 分钟):改个名字
- 输入:在项目 1 代码中,把 Student("小明", 85) 改成你的名字,成绩改成你喜欢的数字
- 预期输出:打印出你的名字和成绩等级
- 提示:只需要改创建对象的那一行
练习 2(2 分钟):加个判断
- 输入:在项目 1 的 get_grade 方法里,加一行判断:如果成绩是满分(100),返回 "S" 级别
- 预期输出:Student("学霸", 100).get_grade() 返回 "S"
- 提示:在 if self.score >= 90 之前加一个 if self.score == 100
练习 3(5 分钟):处理新数据
- 输入:手动构造一个包含 4 个学生的列表(不用 CSV),求平均分
- 预期输出:打印平均分,保留 1 位小数
- 提示:可以用 students = [Student("小A", 80), Student("小B", 90), ...]
练习 4(5 分钟):串起两个项目
- 输入:用 Student 类处理以下数据: [("小红", 92), ("小明", 78), ("小刚", 85)]
- 预期输出:打印每个人的等级,并统计有多少人达到 A 级
- 提示:用列表推导式创建对象
练习 5(3 分钟):看懂报错
- 输入:运行以下代码,会报什么错?
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
s = Student("小明", "85") # 注意:85是字符串
print(s.score + 5)
- 预期输出:报错
TypeError: can only concatenate str (not "int") to str - 提示:
"85" + 5是字符串和整数相加,Python 不允许
📚 作业:做一个「个人消费记录器」
需求描述:做一个命令行记账工具,记录你每天的花销,自动统计本周消费和最高消费项。
功能点:
1. 能添加消费记录(描述 + 金额)
2. 能查看所有记录
3. 能统计「总消费」和「最高一笔消费」
加分项:
1. 能按金额筛选(比如只看大于 100 元的消费)
2. 能把记录保存到文件(JSON 格式),下次启动时加载
验收标准:
- 能跑起来(python my_accountant.py)
- 添加记录后能正确统计
- 代码有注释(每段代码干啥的)
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
这一章 3 个核心点:
1. class 是「模具」,__init__ 是「模具的报到登记处」,self 是「当前浇筑出来的这个产品」
2. 实例属性是「每个人自己的」,类属性是「大家共享的」
3. 把数据 + 操作打包成对象,代码更清晰,更容易维护
延伸学习资源:
- 官方文档:https://docs.python.org/zh-cn/3/tutorial/classes.html (Python 官方类入门)
- 《Python编程:从入门到实践》第 9 章(面向对象基础)
- 视频:B 站搜索「Python 面向对象 通俗讲解」(配合视频学习更直观)
互动钩子:你在写「学生成绩管理系统」或者「记账软件」的时候,有没有遇到过「对象和变量打架」的困惑?评论区聊聊,老粉优先回复!
下一章我们要解决一个问题:现在你能把数据和功能打包了,但如果有些数据我不想让人随便改怎么办? 比如成绩,我不想让用户直接 student.score = 200(改成 200 分),这不合常理。下一章的「属性装饰器 @property」就是来解决这个"属性保护"问题的——敬请期待!

评论(0)