第7章 7.1 class 与 init:面向对象的第一步

全文约 5000 字,阅读约 90 分钟


🎯 开场 3 分钟:为什么要学这个?

上一章我们学会了用 printpdblogging 三招来调试代码,终于能看懂程序为什么会跑偏了。但调试只是"事后补救",你有没有想过:如果一开始就把代码组织得更好,是不是就不需要天天调试了?

想象一下这个场景:你是一个班主任,要管理 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 就是浇筑出来的具体产品(对象)

配图1 - 配图1


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 在小明的场合就是 "小明",在小红的场合就是 "小红"

配图2 - 配图2


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」就是来解决这个"属性保护"问题的——敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。