7-面向对象
约 4778 字大约 16 分钟
2026-10-31
经过前六章的学习,我们已经能写出相当有模样的程序了:变量和容器负责装数据,函数负责封装逻辑,模块负责打包管理。回头看看第六章开发的student_manager模块,七个函数各司其职,运转良好。但用得多了,一个别扭的地方会越来越明显:数据和行为是"分家"的。学生的姓名、成绩装在字典里,而操作这些数据的函数却排成一排站在外面,每次调用都要把数据当作参数传来传去——find_by_name(student_list, name)要传名单,show_student(student, subjects)还要再传一张科目表。数据和操作它的函数明明是"一家人",却住在两个屋檐下,日子久了难免出错。
面向对象编程(Object-Oriented Programming,简称OOP)就是解决这个问题的思想:把相关的数据和函数装进同一个"胶囊"里,这个胶囊就叫对象(Object)。描述"对象长什么样"的模板叫类(Class)。可以打个比方:类就像一张建筑图纸,规定了房子有几个房间、门窗开在哪里;而对象就是照着图纸盖出来的一栋栋房子——图纸只有一张,房子可以盖无数栋,每栋房子里住的数据各不相同。
本章分两步走:第一步学习类与对象的基本用法,学会"造胶囊";第二步学习封装、继承、多态三个进阶特性,把胶囊造得更安全、更省力。学完本章,上一章的函数版成绩管理将被重构成类版本,你会发现代码不仅没变复杂,反而更清爽了。
【学习目标】
知识目标
理解类与对象的概念,掌握"类是模板、对象是实例"的关系。
掌握class定义类、__init__初始化对象、self指代对象自身的语法。
掌握实例属性和实例方法的定义与使用。
理解封装的思想,掌握私有属性的定义与访问方法。
掌握继承的语法和super()调用父类方法的用法。
了解方法重写与多态的表现形式。
能力目标
能够根据需求定义类,创建对象并调用其属性和方法。
能够通过__str__方法自定义对象的打印格式。
能够运用私有属性保护对象内部数据。
能够通过继承减少重复代码,并重构已有的函数版程序。
素养目标
培养抽象思维,学会从具体事物中归纳出"模板"。
培养封装意识,体会"数据与操作绑定"的工程价值。
养成不绕过接口直接篡改数据的规范习惯。
1.类与对象
1.1 为什么需要面向对象
先回顾一下第六章student_manager模块的使用方式:
代码 7-1:数据与操作"分家"的现状
import student_manager
students = [] # 数据在这里
student = student_manager.create_student("张三", 85, 92, 78, 96) # 函数在那头
student_manager.add_student(students, student) # 又传回来
result = student_manager.find_by_name(students, "张三") # 再传进去短短四行代码,student_list这个数据在函数之间进进出出了四趟。数据就好比一群没带钥匙的住户,每办一件事都得把"家"整个交给外人,办事的人多了、传错了地方,问题就来了。
面向对象的思路是反过来的:让数据和函数住在同一个屋檐下。把"一名学生"设计成一个对象——姓名、成绩这些数据装在对象内部,show_info()、calc_average()这些函数也定义在对象身上,调用时直接"对象.方法()",数据不必再四处流浪。这个"装数据又装函数"的模板,就是类。
1.2 定义类与创建对象
用class关键字定义类,语法和def定义函数很相似:
代码 7-2:最简单的类
class Student:
pass # 先定义一个空类,pass表示"什么都没有"
stu1 = Student() # 类名() 就创建了一个对象
stu2 = Student() # 同一张"图纸"可以造出无数个对象
print(stu1) # <__main__.Student object at 0x000001A2B3C4D5E0>
print(stu1 == stu2) # False —— 两个对象是两栋不同的"房子"
print(type(stu1)) # <class '__main__.Student'>注意三点:类名按惯例用大驼峰写法(每个单词首字母大写,如Student、BankAccount);"类名()"这个动作叫实例化,得到的对象也叫实例;print(stu1)输出的一长串是对象在内存中的地址,每次运行都不一样——现在看着没用,2.1节会把它变成有用的信息。
1.3 __init__方法与self
空类只是个壳,真正的类要在创建对象时就装好数据。这靠一个特殊的方法__init__完成,它的名字前后各有两条下划线:
代码 7-3:带__init__的类
class Student:
def __init__(self, name, scores):
self.name = name # 把name装进对象,起名self.name
self.scores = scores # 把scores装进对象,起名self.scores
stu1 = Student("张三", [85, 92, 78, 96])
print(stu1.name) # 张三
print(stu1.scores) # [85, 92, 78, 96]这个写法有两个初学者必问的问题,值得掰开揉碎讲清楚。
问题一:__init__是什么时候执行的?它在"类名()"创建对象的那一刻自动执行,不需要手动调用。创建Student对象时传入的"张三"和成绩列表,会被__init__原样接住。可以把它理解为对象的"出生手续":一出生就登记好姓名和成绩。
问题二:self是什么?self代表"当前这个对象自己"。Student("张三", ...)创建了对象stu1,那么这次__init__执行时,self就是stu1;下次Student("李四", ...)创建的是stu2,self就是stu2。一句话:谁是点号前面那个对象,self就是谁。也正因为self由Python自动传入,调用时永远不用手动传它——构造时传了name和scores两个实参,__init__的形参却是self、name、scores三个,多出来的self就是对象自己。
1.4 属性与方法
类里面定义的变量叫属性(如self.name),定义的函数叫方法(如show_info)。属性描述对象"有什么",方法描述对象"能做什么":
代码 7-4:属性与方法
class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores
def show_info(self): # 实例方法,第一个参数必须是self
print(f"姓名:{self.name},成绩:{self.scores}")
def calc_average(self):
return round(sum(self.scores) / len(self.scores), 1)
stu1 = Student("张三", [85, 92, 78, 96])
stu1.show_info() # 调用方法:对象.方法()
print(f"平均分:{stu1.calc_average()}") # 有返回值的方法可以用在表达式里
stu1.age = 18 # 对象的属性可以随时追加
print(f"年龄:{stu1.age}")程序运行结果如下:
姓名:张三,成绩:[85, 92, 78, 96]
平均分:87.8
年龄:18方法内部要用对象的属性,必须写成self.name——因为name是"某个对象的"姓名,不指明对象就没有意义。理解self还有一个等价视角:stu1.show_info()本质上是"把stu1交给show_info处理",等价于show_info(stu1)。点号调用只是把对象"点"给了方法,这个被点中的对象就由self接住。
类与对象的常用术语汇总见表7-1。
表 7-1 类与对象术语速查
| 术语 | 含义 | 类比 |
|---|---|---|
| 类(class) | 描述一类对象的模板 | 建筑图纸、月饼模子 |
| 对象/实例 | 按类创建出来的具体事物 | 盖出来的房子、压出的月饼 |
| 属性 | 对象内部的数据 | 房子里的家具 |
| 方法 | 对象内部的函数 | 房子里能做的事 |
| init | 创建对象时自动执行的初始化方法 | 出生登记 |
| self | 方法接收到的"当前对象自己" | 点号前面那个对象 |
| 实例化 | "类名()"创建对象的过程 | 按图纸施工 |
【例题 7-1】第一个类:Student类
题目:定义一个Student类,包含姓名和四门课成绩(语文、数学、英语、Python),能够计算平均分、判断单科成绩等级(沿用第5、6章的等级规则)、打印完整信息,并用它管理一名学生的数据。
代码 7-5:Student类完整实现
class Student:
"""学生类:管理一名学生的姓名与四门课成绩"""
def __init__(self, name, scores):
self.name = name
self.scores = scores # 形如 [85, 92, 78, 96]
def calc_average(self):
"""计算平均分"""
return round(sum(self.scores) / len(self.scores), 1)
def get_grade(self, score):
"""判断单科成绩等级"""
if score >= 90:
return "优秀"
elif score >= 80:
return "良好"
elif score >= 70:
return "中等"
elif score >= 60:
return "及格"
else:
return "不及格"
def show_info(self):
"""打印学生完整信息"""
subjects = ["语文", "数学", "英语", "Python"]
print("姓名:" + self.name)
i = 0
for score in self.scores:
grade = self.get_grade(score) # 方法之间也可以用self互相调用
print(f" {subjects[i]}:{score}分({grade})")
i += 1
print(f" 平均分:{self.calc_average()}分")
# ---------- 使用这个类 ----------
stu = Student("张三", [85, 92, 78, 96])
stu.show_info()程序运行效果如下:
姓名:张三
语文:85分(良好)
数学:92分(优秀)
英语:78分(中等)
Python:96分(优秀)
平均分:87.8分对比第5章的例题5-2:同样是calc_average、get_grade这组功能,函数版要把scores作为参数传来传去,而类版把数据和功能绑在了一起——一个Student对象"自己知道"自己的成绩,"自己会算"平均分。这就是"数据和操作住在同一个屋檐下"的直观感受。
2.类的进阶特性
2.1 __str__方法:让对象打印得像样
代码 7-6:打印对象的尴尬
class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores
stu = Student("李四", [70, 88, 95, 81])
print(stu) # <__main__.Student object at 0x0000021B4C3E8D30>print一个对象,得到的是一串内存地址,对人毫无意义。其实print()打印对象时,会先问对象要一个"自我介绍"——默认的介绍就是那串地址。我们可以定义__str__方法,重写这份介绍:
代码 7-7:定义__str__方法
class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores
def __str__(self):
avg = round(sum(self.scores) / len(self.scores), 1)
return f"Student(姓名:{self.name},平均分:{avg})"
stu = Student("李四", [70, 88, 95, 81])
print(stu) # Student(姓名:李四,平均分:83.5)只要print()碰到对象,就会自动调用它的__str__并打印返回的字符串。这个方法和__init__一样,属于Python预先约定好的"特殊方法",名字前后各两条下划线,需要时定义、从不手动调用。第3章学的f-string在这里大显身手——格式化对象信息,__str__是最常见的舞台。
2.2 封装与私有属性
先看一段"谁都能改"的危险代码:
代码 7-8:直接改属性的风险
stu = Student("王五", [90, 90, 90, 90])
stu.scores = "九十分" # 外部代码可以随意篡改属性
print(stu.scores) # 九十分 —— 成绩居然变成了字符串属性对外完全敞开,任何一段代码都能把它改成任何值——成绩变成字符串,后面所有sum()、len()的调用全会崩溃。防住这种隐患的思想就叫封装:把数据藏起来,只通过规定的方法进出。好比银行的钱不会让你直接伸手进金库,必须通过柜台办理——柜台会检查你的操作合不合法。
Python实现"藏"的方式很朴素:属性名前加两条下划线,它就变成了私有属性,外部无法直接访问:
代码 7-9:私有属性与访问方法
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # 双下划线开头:私有属性
def deposit(self, amount):
"""存款:金额必须为正"""
if amount <= 0:
print("存款金额必须大于0")
return
self.__balance += amount
def withdraw(self, amount):
"""取款:不能超过余额"""
if amount > self.__balance:
print("余额不足,取款失败")
return
self.__balance -= amount
def get_balance(self):
"""查询余额"""
return self.__balance
account = BankAccount("张三", 1000)
account.deposit(500)
print(account.get_balance()) # 1500
account.withdraw(2000) # 余额不足,取款失败(柜台把不合法操作挡了回去)
print(account.get_balance()) # 1500
# print(account.__balance) # 报错!外部访问不了私有属性__balance被藏进了"金库",存多少、取多少、能不能取,全由deposit()和withdraw()这两个"柜台"审查。类内部的方法(self所在的方法)不受限制,照样能访问私有属性。这种"数据藏起来、操作走接口"的习惯,是编写可靠程序的必修素养——第9章操作数据库时,同样讲究"该查的查、该防的防"。
2.3 继承
写Person类时定义了name、age和introduce();接着写Student类,发现学生也有姓名、年龄,也要自我介绍——这些代码再抄一遍吗?继承就是解决重复的方案:子类自动拥有父类的全部属性和方法,只需再添上自己的新内容。
代码 7-10:继承与super()
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
print(f"我叫{self.name},今年{self.age}岁")
class Student(Person): # 括号里写父类:Student继承Person
def __init__(self, name, age, school):
super().__init__(name, age) # 把name、age交给父类的__init__处理
self.school = school # 再添加自己独有的属性
def study(self):
print(f"{self.name}正在{self.school}学习Python")
s = Student("张三", 18, "电子信息系")
s.introduce() # 我叫张三,今年18岁 —— 从父类继承来的方法,直接就能用
s.study() # 张三正在电子信息系学习Python —— 子类自己的方法class Student(Person)声明了继承关系:Person是父类,Student是子类,子类"继承家业",父类的属性和方法全部到手。子类的__init__里通过super().init(name, age)调用父类的初始化逻辑,自己只补充school这一项新内容——父类的活不用重干,这正是不写重复代码的关键。
如果子类对继承来的方法不满意,可以定义同名方法把它覆盖掉,这叫方法重写:
代码 7-11:方法重写
class Student(Person):
def __init__(self, name, age, school):
super().__init__(name, age)
self.school = school
def introduce(self): # 重写父类的introduce
print(f"我是{self.school}的学生{self.name}")
s = Student("李四", 19, "职业技术学院")
s.introduce() # 我是职业技术学院的学生李四(执行的是重写后的版本)继承的常用语法汇总见表7-2。
表 7-2 继承常用语法
| 写法 | 作用 |
|---|---|
| class 子类(父类): | 声明继承,子类自动拥有父类的属性和方法 |
| super().init(...) | 在子类__init__中调用父类的初始化逻辑 |
| 在子类中定义同名方法 | 方法重写,覆盖父类版本 |
| 对象.方法() | 优先调用子类自己的,没有才找父类 |
2.4 多态简讲
方法重写带来一个有趣的现象:不同的子类对象调用同一个方法名,表现出各自不同的行为——这就是多态(Polymorphism),字面意思"多种形态"。
代码 7-12:同一指令,各自发挥
class Person:
def __init__(self, name):
self.name = name
def introduce(self):
print(f"我叫{self.name},是一名普通公民")
class Teacher(Person):
def introduce(self):
print(f"我叫{self.name},是一名教师,教书育人")
class Student(Person):
def introduce(self):
print(f"我叫{self.name},是一名学生,努力学习")
people = [Person("老王"), Teacher("李老师"), Student("张三")]
for p in people:
p.introduce() # 同一句调用,三种输出程序运行结果如下:
我叫老王,是一名普通公民
我叫李老师,是一名教师,教书育人
我叫张三,是一名学生,努力学习循环里只有一句p.introduce(),执行时却各说各的词——Python不关心p具体是什么类型,只关心它有没有introduce方法。这种"不看身份看能力"的风格被戏称为鸭子类型:"只要走起来像鸭子、叫起来像鸭子,那它就是鸭子。"多态在本阶段了解即可,不要求展开掌握。
【例题 7-2】用类重构班级成绩管理
题目:第六章用"字典+函数"开发了student_manager模块。现在用面向对象的思想重构它:定义Student类管理单个学生的数据,定义StudentList类管理整个班级,实现添加、查找、统计各科平均分、总分排名、找总分冠军等功能,并对比两版的代码感受类的优势。
代码 7-13:两个类的定义(保存为student.py)
class Student:
"""学生类:一个对象对应一名学生"""
def __init__(self, name, scores):
self.name = name
self.scores = scores # [语文, 数学, 英语, Python]
self.total = sum(scores)
self.average = round(self.total / len(scores), 1)
def __str__(self):
return f"{self.name} 总分{self.total} 平均分{self.average}"
class StudentList:
"""班级类:管理一批学生对象"""
def __init__(self):
self.students = [] # 装Student对象
def add(self, student):
"""添加学生"""
self.students.append(student)
def find_by_name(self, name):
"""按姓名查找,找到返回对象,找不到返回None"""
for stu in self.students:
if stu.name == name:
return stu
return None
def subject_avg(self):
"""计算班级各科平均分,返回字典"""
count = len(self.students)
if count == 0:
return {}
keys = ["语文", "数学", "英语", "Python"]
totals = {"语文": 0, "数学": 0, "英语": 0, "Python": 0}
for stu in self.students:
for i in range(len(keys)):
totals[keys[i]] += stu.scores[i]
result = {}
for subject in totals:
result[subject] = round(totals[subject] / count, 1)
return result
def ranking(self):
"""按总分从高到低排名(第5章的lambda在此登场)"""
return sorted(self.students, key=lambda stu: stu.total, reverse=True)
def top_student(self):
"""找出总分最高的学生"""
if len(self.students) == 0:
return None
top = self.students[0]
for stu in self.students:
if stu.total > top.total:
top = stu
return top代码 7-14:使用两个类
from student import Student, StudentList # 把类文件当模块导入——第6章的知识
banji = StudentList()
# 1. 添加学生
banji.add(Student("张三", [85, 92, 78, 96]))
banji.add(Student("李四", [70, 88, 95, 81]))
banji.add(Student("王五", [60, 74, 82, 90]))
banji.add(Student("赵六", [55, 66, 71, 68]))
# 2. 查找学生(注意:不再需要传students参数)
stu = banji.find_by_name("李四")
print("查找结果:" + str(stu))
# 3. 班级各科平均分
print()
print("班级各科平均分:")
for subject, avg in banji.subject_avg().items():
print(f" {subject}:{avg}分")
# 4. 总分排名
print()
print("总分排名:")
rank = 1
for stu in banji.ranking():
print(f" 第{rank}名:{stu}")
rank += 1
# 5. 总分最高的学生
top = banji.top_student()
print()
print(f"总分最高:{top.name}({top.total}分)")程序运行效果如下:
查找结果:李四 总分334 平均分83.5
班级各科平均分:
语文:67.5分
数学:80.0分
英语:81.5分
Python:83.8分
总分排名:
第1名:张三 总分351 平均分87.8
第2名:李四 总分334 平均分83.5
第3名:王五 总分306 平均分76.5
第4名:赵六 总分260 平均分65.0
总分最高:张三(351分)两版对照,优势一目了然。函数版里数据是"公共财产",七个函数每调用一次就要传一次参数;类版里数据成了对象"自带的行李",banji.add(...)、banji.find_by_name(...)的调用方式更自然,Student对象还通过__str__自带打印格式。这个例题是全书前半部的一次"总装":字典与列表的存储(第4章)、循环与条件(第2章)、sorted加lambda排序(第5章)、模块导入(第6章)、f-string格式化(第3章)全部各就各位。
不过这个程序还有一个美中不足:程序一关,四名学生就"人间蒸发"了,下次运行得重新录入。怎么让数据留下来?下一章的文件操作就是答案。
