Python面向对象编程:类与继承核心知识详解
初学 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 fPerson(name{self.name}, age{self.age})p1 Person(Alice, 20) print(p1) # Person(nameAlice, age20)__str__面向用户__repr__面向开发者。在调试时repr()通常用于展示更完整的对象信息。实际项目中如果希望日志和打印都友好建议两个方法都实现一下。4. 继承的核心机制4.1 继承解决什么问题开发学生管理系统时你可能会发现Student和Teacher都需要姓名、年龄、自我介绍等能力。如果为每个类重复编写同样的代码既浪费时间又难以统一维护。继承允许我们定义一个通用的父类基类然后在子类中复用父类的属性与方法同时扩展自己的特性。Person父类通用属性name, age ├── Student子类增加 student_id └── Teacher子类增加 course4.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与issubclassPython 提供了两个内置函数帮助检查对象与类的关系class Person: pass class Student(Person): pass s1 Student() print(isinstance(s1, Student)) # Trues1 是 Student 实例 print(isinstance(s1, Person)) # TrueStudent 继承自 Person print(issubclass(Student, Person)) # TrueStudent 是 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(fBob 的平均成绩{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, scoresNone): 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__是低成本、高收益的习惯。哪怕只是简单返回fStudent(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 面向对象的理解会更扎实。
