初学 Python 时,很多人都有过这样的经历:函数写了一堆,逻辑能跑通,可一旦业务复杂起来,代码就开始“失控”——要么重复代码太多,要么改一个需求要牵连好几个函数。
这些都是从面向过程编程转向面向对象编程时必经的坎。本文将围绕MIT 6.0001 计算机科学导论第9讲的内容,系统整理 Python 中类(Class)和继承(Inheritance)的核心知识点,用大量可运行的代码示例,带你把 OOP 的地基打扎实。
无论你是刚看完 Python 基础语法的新手,还是想系统回顾面向对象特性的开发者,这篇文章都值得认真读完。你不仅能学会如何定义类、创建对象、理解继承和多态,还能掌握实际项目中关于类设计的通用建议。
1. 为什么要学习类和继承
1.1 从面向过程到面向对象
Python 首先是一门面向过程的语言,写脚本时我们习惯从上到下、按函数拆分子任务。但现实世界的软件业务往往涉及大量实体和状态,比如:
- 学生有姓名、学号、成绩;
- 银行账户有余额、卡号、存取操作;
- 订单有商品、金额、用户、支付状态。
如果都用单独的变量和函数管理,数据分散且不易维护。面向对象编程(OOP)的核心思想是:把数据和对数据的操作封装在一起,形成一个“对象”。
类(Class)就是对象的模板,对象(Object)是模板的具体实例。用类组织代码后,你会明显感受到三个好处:
- 数据更聚合,状态和行为放在同一处。
- 代码复用性更高,多个实例共享类结构。
- 逻辑更贴近真实世界,建模和后期维护都更直观。
1.2 类和对象的直观理解
用一个简单的比喻:类是建筑的施工图,对象是按照施工图盖出来的楼。
施工图里定义了每层有几个房间、每间房多大面积,但施工图本身不是房子。我们同一个施工图可以盖出很多栋结构相同的楼,每栋楼住的人不同,房间摆设也不同。
在 Python 中,类的定义并不占用真正的业务数据,只有实例化后,对象才会真正“住进数据”。这种设计带来的好处是:我们只用编写一次类,就可以创建多个互不干扰的对象。
1.3 为什么 MIT 6.0001 第9讲专门讲类和继承
MIT 6.0001 是麻省理工学院计算机科学导论课程,使用的语言正是 Python。在第9讲之前,课程已经完成了基本语法、函数、递归、调试等基础教学;第9讲则是从“过程化编程”跨入“面向对象编程”的关键节点。
这一讲的核心目标有三个:
- 学会定义类、创建对象、理解实例变量和方法。
- 理解继承如何实现“子类复用父类代码”。
- 通过案例体会 OOP 在模拟真实系统时的优势。
后面第10讲、第11讲会继续深入面向对象设计和程序调试,所以第9讲是承上启下的一课。
2. 环境准备与版本说明
类和继承属于 Python 语言核心语法,几乎不需要第三方库。只要你的电脑上能运行 Python 3,就可以直接运行本文所有示例。
2.1 语言版本选择
本文示例基于 Python 3。Python 2 官方早已停止维护,不建议新项目继续使用。
你可以通过命令行查看版本:
python --version如果输出是Python 3.x.x,说明环境正常。如果你还没有安装 Python,可以到 Python 官网下载对应操作系统的安装包。
2.2 运行方式说明
- 如果你用命令行,将代码保存为
.py文件,然后执行:
python demo.py- 如果你使用 Jupyter Notebook 或 VS Code,直接逐行运行即可。
- 在学习类的时候,强烈建议打开一个 Python 交互式环境(
python进入 REPL),边敲边观察结果。
2.3 本文代码项目结构
为了阅读方便,下文示例会按“概念示例”和“完整实战”两个层次组织。完整实战部分会使用以下结构:
course_system/ ├── models.py # 定义相关类 ├── main.py # 运行入口 └── README.md # 项目说明(可选)你可以先建立一个自己的练习目录,把每个示例保存为独立文件,便于反复修改测试。
3. Python 类的基础语法
在深入继承之前,我们要先掌握类的基本定义方式。
3.1 定义一个最简单的类
Python 中使用class关键字定义类,类名通常使用大驼峰命名法(CamelCase)。
# 文件路径:course_system/models.py class Person: pass这段代码定义了一个空类Person。pass表示暂时什么都不做,在 Python 中用于占位。
创建对象的方式是:类名后加括号。
p1 = Person() p2 = Person() print(p1) # 输出类似 <__main__.Person object at 0x...> print(p2)此时p1和p2是两个完全独立的对象。即使它们来自同一个类,修改其中一个的属性不会影响另一个。
3.2__init__方法与self
空类意义不大。我们希望每个对象在创建时就能拥有自己的数据,比如Person应该有一开始就知道的姓名和年龄。
Python 通过__init__方法完成初始化。它会在对象创建后自动被调用,第一个参数固定是self,代表当前被创建的对象本身。
class Person: def __init__(self, name, age): self.name = name self.age = age创建对象:
p1 = Person("Alice", 20) p2 = Person("Bob", 21) print(p1.name) # Alice print(p2.age) # 21这里的self.name = name表示:在当前对象上添加一个名为name的实例变量,并把传入参数的值赋给它。
需要注意,__init__不是构造函数,而是初始化方法。真正分配对象内存的步骤由 Python 内部完成,__init__只是负责给新对象设置初始状态。
3.3 实例方法与属性
对象可以保存数据,也可以拥有行为。在类中定义的函数叫方法,方法的第一个参数通常也是self,这样方法内部才能访问当前对象的数据。
class Person: def __init__(self, name, age): self.name = name self.age = age def introduce(self): return f"我叫{self.name},今年{self.age}岁。" def birthday(self): self.age += 1调用方式:
p1 = Person("Alice", 20) print(p1.introduce()) # 我叫Alice,今年20岁。 p1.birthday() print(p1.age) # 21实例方法通过对象.方法名()调用,Python 会自动把对象本身作为第一个参数传入。所以p1.introduce()等价于Person.introduce(p1)。
这一点新手经常忽略:定义方法时如果不写self,调用时往往会出现TypeError: introduce() takes 0 positional arguments but 1 was given之类的报错。
3.4 属性访问控制与命名习惯
Python 没有 Java 中private、public这样的强制访问控制,而是依赖命名约定:
name:普通公开属性,外部可以直接访问。_name:约定为“受保护”属性,提醒外部不要随意访问,但 Python 不会强制拦截。__name:双下划线开头会触发名称改写,在类内部仍然可以访问,外部直接访问会报错,主要用于避免子类中意外覆盖。
class Person: def __init__(self, name, age): self._name = name # 受保护命名 self.__age = age # 名称改写 def get_age(self): return self.__age外部访问__age时会发现属性不存在,但这并不是严格意义上的私有变量,只是 Python 对名称做了改写(变成了_Person__age)。因此不要依赖双下划线做安全控制,它更像一个“请勿随意访问”的标识。
3.5 特殊方法__str__与__repr__
直接打印对象时,默认输出是对象的内存地址字符串,不够友好。我们可以通过__str__方法控制打印效果。
class Person: def __init__(self, name, age): self.name = name self.age = age def __str__(self): return f"Person(name={self.name}, age={self.age})"p1 = Person("Alice", 20) print(p1) # Person(name=Alice, age=20)__str__面向用户,__repr__面向开发者。在调试时,repr()通常用于展示更完整的对象信息。实际项目中如果希望日志和打印都友好,建议两个方法都实现一下。
4. 继承的核心机制
4.1 继承解决什么问题
开发学生管理系统时,你可能会发现Student和Teacher都需要姓名、年龄、自我介绍等能力。如果为每个类重复编写同样的代码,既浪费时间,又难以统一维护。
继承允许我们定义一个通用的父类(基类),然后在子类中复用父类的属性与方法,同时扩展自己的特性。
Person(父类,通用属性:name, age) ├── Student(子类,增加 student_id) └── Teacher(子类,增加 course)4.2 子类的定义与super()
继承的语法是在类名后加括号,括号中写父类。
class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id = student_id这里super().__init__(name, age)的作用是:调用父类Person的__init__方法,先完成name和age的初始化,再为子类补充student_id。
如果不调用super(),父类的__init__不会自动执行,子类对象就没有父类中定义的属性。
s1 = Student("小明", 20, "20230001") print(s1.name) # 小明 print(s1.student_id) # 202300014.3 方法重写
子类可以定义与父类同名的方法,这种行为叫覆盖。调用子类对象的方法时,会优先找到子类自己的版本。
class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id = student_id def introduce(self): return f"我叫{self.name},是一名学生,学号是{self.student_id}。"s1 = Student("小明", 20, "20230001") print(s1.introduce()) # 我叫小明,是一名学生,学号是20230001。如果仍然想调用父类的方法,可以在子类方法中主动调用super().方法名():
class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id = student_id def introduce(self): parent_info = super().introduce() return f"{parent_info} 我的学号是{self.student_id}。"4.4isinstance与issubclass
Python 提供了两个内置函数帮助检查对象与类的关系:
class Person: pass class Student(Person): pass s1 = Student() print(isinstance(s1, Student)) # True,s1 是 Student 实例 print(isinstance(s1, Person)) # True,Student 继承自 Person print(issubclass(Student, Person)) # True,Student 是 Person 的子类这里isinstance(s1, Person)返回True是理解多态的关键:子类对象可以看作父类类型的一种。
4.5 多重继承
Python 支持一个子类继承多个父类,但实际使用时要非常谨慎。
class A: def method(self): print("A method") class B: def method(self): print("B method") class C(A, B): pass c = C() c.method() # A method当父类中存在同名方法时,Python 会按照方法解析顺序(MRO)查找。可以通过类名.__mro__查看查找顺序:
print(C.__mro__) # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)MRO 的完整规则较复杂,日常开发中如果出现复杂的多重继承结构,往往意味着类设计需要重新审视。
5. 完整实战案例:校园人员管理系统
下面我们用类和继承写一个完整的“校园人员管理系统”,把这一讲的核心知识点串起来。
5.1 需求分析
系统需要管理三类人:通用人员、学生、教师。
- 通用人员:有姓名、年龄、基本介绍能力。
- 学生:在通用人员基础上增加学号,并能计算班级平均分(简化场景)。
- 教师:在通用人员基础上增加课程字段,并能打印课程信息。
功能要求:
- 创建一个学生对象和一个教师对象。
- 分别调用各自的介绍方法。
- 展示继承、重写和
super()的实际用法。
5.2 定义基础类:Person
# 文件路径:course_system/models.py class Person: """通用人员类,所有人员类型的基类""" def __init__(self, name, age): self.name = name self.age = age def introduce(self): return f"我叫{self.name},今年{self.age}岁。" def __str__(self): return self.introduce()说明:introduce是通用行为,子类可以根据需要重写。
5.3 定义学生类:Student
# 文件路径:course_system/models.py class Student(Person): """学生类,继承 Person""" def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id = student_id def introduce(self): # 先复用父类介绍逻辑,再补充学生特有内容 parent_info = super().introduce() return f"{parent_info} 我是一名学生,学号是{self.student_id}。" def add_score(self, score): """给学生添加一门课程成绩,简单维护在一个列表中""" if not hasattr(self, "scores"): self.scores = [] self.scores.append(score) def average_score(self): """计算平均分,没有成绩时返回 0""" if not hasattr(self, "scores") or len(self.scores) == 0: return 0 return sum(self.scores) / len(self.scores)这里用hasattr判断对象上是否已经存在scores属性,避免在__init__中初始化列表时出现“属性共享”的坑。更规范的做法是直接在__init__中设置self.scores = [],下面的代码也会这么做。
为了代码更清晰,我们把__init__改一下:
class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id = student_id self.scores = [] def add_score(self, score): self.scores.append(score) def average_score(self): if not self.scores: return 0 return sum(self.scores) / len(self.scores)这样做更符合常规习惯,也不容易出现可变默认参数问题。
5.4 定义教师类:Teacher
# 文件路径:course_system/models.py class Teacher(Person): """教师类,继承 Person""" def __init__(self, name, age, course): super().__init__(name, age) self.course = course def introduce(self): parent_info = super().introduce() return f"{parent_info} 我是一名教师,负责讲授{self.course}课程。" def give_lecture(self): return f"今天开始讲:{self.course}课程的第一章。"5.5 编写运行入口
# 文件路径:course_system/main.py from models import Student, Teacher def main(): alice = Student("Alice", 20, "20230001") bob = Student("Bob", 21, "20230002") prof = Teacher("Wang", 35, "计算机科学导论") alice.add_score(90) alice.add_score(85) bob.add_score(78) print(alice.introduce()) print(bob.introduce()) print(prof.introduce()) print(f"\nAlice 的平均成绩:{alice.average_score()}") print(f"Bob 的平均成绩:{bob.average_score()}") print(f"\n{prof.name} 即将开始上课:{prof.give_lecture()}") if __name__ == "__main__": main()5.6 运行与预期输出
在终端执行:
cd course_system python main.py预期输出:
我叫Alice,今年20岁。 我是一名学生,学号是20230001。 我叫Bob,今年21岁。 我是一名学生,学号是20230002。 我叫Wang,今年35岁。 我是一名教师,负责讲授计算机科学导论课程。 Alice 的平均成绩:87.5 Bob 的平均成绩:78 Wang 即将开始上课:计算机科学导论课程的第一章。整个案例展示了:
- 父类
Person统一管理通用属性name、age。 - 子类通过
super().__init__()复用父类初始化逻辑。 - 子类通过重写
introduce()展示不同身份的介绍内容。 - 对象本身承载数据,实例之间互不影响。
6. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
调用子类方法时报TypeError: __init__() missing 1 required positional argument | 子类__init__里忘了调用super().__init__(),导致父类属性未初始化 | 在子类__init__第一行使用super().__init__(...)初始化父类属性 |
方法定义时没写self,调用时报参数数量错误 | Python 方法调用时会自动传入对象本身作为第一个参数 | 定义实例方法时第一个参数写self |
| 两个对象之间共享了列表或字典属性 | 在类中直接定义可变变量,如scores = [],而不是通过self定义实例变量 | 在__init__中使用self.scores = [],避免所有实例共用同一个列表 |
打印对象时显示地址字符串<__main__.Person object at ...> | 没有实现__str__或__repr__ | 在类中定义__str__方法,返回可读字符串 |
调用父类方法时出现AttributeError: 'Student' object has no attribute 'name' | 子类__init__里没有调用父类初始化 | 确保子类__init__中先执行super().__init__() |
| 多重继承下方法调用不符合预期 | 父类之间存在同名方法,MRO 顺序与直觉不同 | 使用ClassName.__mro__查看查找顺序;如果结构太复杂,优先考虑组合而不是多重继承 |
6.1 经典坑:可变默认参数
这是初学者经常忽略的问题。在看下面这段代码前,先思考输出是什么:
class Student: def __init__(self, name, scores=[]): self.name = name self.scores = scores s1 = Student("Alice") s1.scores.append(90) s2 = Student("Bob") print(s2.scores) # ?完整解释:Python 在函数定义时只会创建一次默认列表,所有没传scores参数的对象都会引用同一个列表。因此s2.scores会把Alice的成绩也打印出来。
正确写法:
class Student: def __init__(self, name, scores=None): self.name = name self.scores = scores if scores is not None else []实际项目中,凡是用可变对象(列表、字典、集合)做默认值,都应该警惕。
6.2 排查清单
如果你在练习中遇到了问题,可以按以下顺序排查:
- 检查类名是否拼写正确,Python 是大小写敏感的语言。
- 检查
__init__是否写成了_init_,少写下划线是一个非常典型的笔误。 - 检查子类是否调用了
super().__init__()。 - 检查方法内部是否使用
self.属性名访问实例变量。 - 打印
对象.__dict__,该属性会展示对象所有实例变量,非常方便排查状态问题。
7. 最佳实践与工程建议
类与继承虽然好用,但如果不加约束,很容易写出难以维护的代码。以下建议来自日常项目和课程实践,供你参考。
7.1 优先组合,谨慎继承
继承能复用代码,但也会引入紧耦合。父类一旦变化,所有子类都可能受影响。
如果一个类只是“需要某个能力”,而不是“某个能力的子类型”,那么组合更合适:
class Course: def __init__(self, name, teacher): self.name = name self.teacher = teacher此时Course中持有Teacher对象,而不是继承Teacher。这个原则在团队协作中尤其重要,它可以显著降低代码间的耦合度。
7.2 合理地使用方法重写
重写方法时,尽量通过super().方法名()复用父类逻辑,避免完全丢弃原有行为。这样做的好处是:
- 父类的功能仍然生效。
- 后续父类修复 bug,子类自动受益。
- 代码意图更清晰:读者能看到“先做父类的事,再做子类特有的事”。
7.3 不要在类级别定义可变对象
以下写法是错误示范:
class Student: scores = [] # 所有实例共享同一个列表正确做法:
class Student: def __init__(self, name): self.name = name self.scores = []如果确实需要类级别的共享数据,请用不可变对象或者明确说明它是类变量,而不是实例变量。
7.4 利用特殊方法提升可调试性
实现__str__和__repr__是低成本、高收益的习惯。哪怕只是简单返回f"Student(name={self.name})",也会让调试日志清晰很多。
7.5 多态是面向对象的灵魂
多态可以概括为:相同的方法名,在不同类型的对象上表现出不同的行为。
在校园系统示例中,alice.introduce()和prof.introduce()调用的是同一个方法名,但输出完全不同。实际项目里,一个函数可以对所有 Person 子类统一处理:
def print_introduce(person): print(person.introduce()) print_introduce(alice) print_introduce(prof)这种方式让代码更有弹性:未来增加新人员类型,只要它继承了Person并实现或继承了introduce,现有调用代码就不需要修改。
7.6 命名规范建议
- 类名使用大驼峰,例如
StudentManager。 - 方法名和变量名使用小写下划线,例如
average_score。 - 私有不希望外部直接访问的属性可以加一个下划线前缀,例如
_internal_data。 - 避免使用
list、dict、id等内置名称作为变量名。
8. 总结与下一步学习
本文从 MIT 6.0001 第9讲的主题出发,系统梳理了 Python 类和继承的知识框架:
- 类使用
class定义,对象通过类名()创建。 __init__负责初始化实例状态,self表示当前对象。- 实例方法默认接收
self参数,通过self.属性名访问实例数据。 - 继承通过
class 子类(父类)实现,子类中通过super()调用父类方法。 - 子类可以重写父类方法,以实现不同的行为,这就是多态的基础。
isinstance和issubclass可以检查对象或类之间的继承关系。
下一步建议你继续学习:
- Python 的
abstractmethod抽象类,了解如何强制子类实现指定方法。 @property装饰器,用更优雅的方式管理属性访问。- 类方法与静态方法(
@classmethod与@staticmethod),理解它们与实例方法的区别。 - 在真实项目中练习:写一个小型命令行学生管理系统,把类、继承、文件读写结合起来。
类与继承不是面试八股,而是日常开发中组织代码的核心手段。动手敲代码是最快的理解方式。建议你按照第5章的校园人员管理系统,自己添加一个Staff(行政人员)类,增加工号属性和打卡方法,再为Student添加一个print_report()方法。完成这些练习后,你对 Python 面向对象的理解会更扎实。