拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python类与继承:从零理解面向对象编程核心

在计算机入门课程里真正让代码从一串线性指令变成可复用模型的转折点往往是类class和继承inheritance。MIT 6.0001 计算机科学导论第 9 讲专门用一整节课来讲 Python 里的类与继承并不是为了教语法而是为了解决一个更现实的问题当程序规模变大、逻辑变复杂时怎么让数据和对数据的操作绑定在一起怎么让相似的代码只写一遍怎么在已有代码基础上扩展新功能而不破坏原有行为。这篇内容会把这一讲的思路拆开讲清楚适合刚学完 Python 基础语法、写过函数但还没系统接触面向对象编程的读者也适合准备复习 MIT 6.0001 学习资料的人。文章后面会给出完整示例代码、运行结果、常见错误排查路径以及一套可以直接套用的类设计练习清单。1. 先理解 Python 里“类”到底在解决什么问题1.1 从过程式代码到对象式代码在还没有使用类之前大多数 Python 程序是“过程式”的用列表、字典保存数据用函数处理数据。小规模脚本这样写没有问题但一旦数据有状态、数据之间存在关系、或者需要反复执行同一套逻辑过程式代码就容易出现两类问题。第一类是数据和操作分离。比如程序里有一个字典表示学生student { name: Alice, id: 2024001, grades: [85, 92, 78] }如果之后需要计算平均成绩就要写一个函数def average_grade(student): return sum(student[grades]) / len(student[grades])问题在于这个函数的适用对象只是“学生”这种字典如果换成另一个结构的字典函数可能就出错。而且函数和数据结构之间没有强绑定关系代码写久了容易忘记某个函数到底接收什么结构。第二类是重复代码难以消除。假设现在有“学生”和“老师”两类数据它们的属性有重合也有差异。过程式写法很容易出现两份结构类似但又不完全一样的代码维护成本随之上升。类解决的核心问题就是把“数据”和“操作这些数据的函数”打包在一起让函数成为对象的行为。这样代码结构更贴近现实世界学生是一个对象它有姓名、学号、成绩这些属性也有计算平均分、查询成绩这些行为。1.2 类与对象的通俗说法和技术定义通俗地说类是图纸对象是图纸生产出来的实物。图纸定义了房子有几个房间、门在哪里、窗户什么尺寸但图纸本身不能住人只有图纸建成的具体房子才能住人。类是定义阶段对象是运行阶段。从技术定义看类是一种数据结构模板它把状态属性和行为方法绑定在一起。对象是类在内存中的实例拥有类定义的属性值和可调用的方法。在 Python 里创建一个类的最小语法是class Person: passclass是关键字Person是类名pass只是一个占位语句表示这个类暂时没有内容。这种方式常用于先搭建骨架后面再补属性和方法。1.3 类、对象、属性、方法之间的关系类由三部分构成类名、属性、方法。属性是对象的数据比如姓名、年龄、成绩。属性分为类属性和实例属性初学时先重点理解实例属性每个对象都有自己的属性值不同对象的同一属性可以不一样。方法是定义在类内部的函数它描述对象能做什么。方法第一参数通常命名self代表调用这个方法的对象本身。可以用一张表来对比它们在类里的作用概念是什么示例访问方式类名类的名称用于创建对象PersonPerson(...)实例属性属于某个对象的数据obj.nameobj.name方法定义在类里的函数def introduce(self)obj.introduce()对象类创建出的具体实例p Person(Alice)变量p这里容易误解的地方是属性不只是变量方法是限定在类范围内的函数。同一个类创建的对象结构相同但属性值可以不同。方法逻辑共享但方法里的self能访问到各自对象的属性。2. 环境准备Python 版本、运行方式和示例目录2.1 环境要求学习 Python 类和继承对环境要求并不高只要本地能运行 Python 3 即可。MIT 6.0001 课程使用的 Python 版本会随学期更新当前学习环境建议使用 Python 3.8 以上版本因为面向对象核心语法在 Python 3 中已经稳定不需要依赖特定新特性。项目推荐配置说明Python 版本3.8支持 dataclass、类型标注等扩展能力编辑器VS Code 或 PyCharm便于断点调试和变量观察操作系统Windows / Linux / macOS 均可语法无平台差异运行方式命令行运行.py文件最简单直接可选工具Jupyter Notebook适合做小片段实验如果还没确认 Python 是否安装可以打开终端执行python --version有些系统里 Python 3 命令是python3python3 --version正常输出类似Python 3.10.12只要主版本是 3 就可以继续。2.2 推荐的项目目录结构即使只是学习一个小例子也建议按一个微型项目来组织文件。这样后续增加类定义、测试代码、运行脚本时目录层次不会混乱。lecture9_oop/ ├── main.py ├── person.py └── README.mdperson.py存放类定义。main.py程序入口负责创建对象、调用方法、打印结果。README.md记录本项目的运行方式和练习目标。初学者常犯的错误是把所有类和测试代码堆在同一个文件里。小型示例可以接受但一旦类变多、逻辑变复杂拆分文件会让代码更清晰。建议从课程学习阶段就开始养成“类文件专门放类定义入口文件专门放运行逻辑”的习惯。2.3 用最小可运行代码确认环境没问题在正式开始写类之前先创建一个最简单的测试文件hello.pyprint(Python environment is ready)运行python hello.py看到输出Python environment is ready说明环境可以正常执行 Python 文件。这一步不是浪费时间它可以提前排除编码、终端乱码、解释器路径等基础问题避免后面把环境问题误判成代码问题。3. 从 MIT 6.0001 第 9 讲的思路出发定义一个最小类3.1 先写一个最简单的 Person 类MIT 6.0001 第 9 讲在引入类时不会立刻讲复杂的继承而是先用一个非常小的类把“定义类、创建对象、调用方法”这条链路走通。在person.py中定义class Person: def __init__(self, name, age): self.name name self.age age def introduce(self): return f{self.name} is {self.age} years old.这个类包含两部分__init__构造函数在创建对象时自动调用用于初始化对象的属性。introduce普通方法返回一个描述该对象的字符串。在main.py中创建对象from person import Person alice Person(Alice, 20) print(alice.introduce())运行python main.py输出结果Alice is 20 years old.这段代码虽然短但完整展示了类的基本流程Person(Alice, 20)创建对象alice.introduce()调用对象方法方法内部通过self.name和self.age访问到对象自己的数据。3.2 理解__init__与self很多初学者第一次看到__init__时会觉得奇怪因为方法名的前后都有双下划线。这类方法在 Python 里叫“特殊方法”它们不是给业务代码直接调用的而是在特定时机由 Python 自动调用。__init__的触发时机是对象被创建的那一刻。例如Person(Alice, 20)执行时Python 会做两步操作分配一块内存准备存放这个新对象。调用Person.__init__把新对象作为第一个参数传入同时传入Alice和20。所以__init__的第一个参数self指的正是“正在被创建的那个对象”。在方法内部写self.name name意思是要把这个对象的name属性设为传入的name参数值。为什么必须用self作为第一个参数这是 Python 的约定。Java 里用this表示当前对象C 也类似Python 则把当前对象显式传给每个实例方法。这个设计让方法调用机制更透明但也导致初学者经常忘记在方法定义里加上self。常见错误写法class Person: def __init__(self, name, age): self.name name self.age age # 错误缺少 self def introduce(): return Hello这种写法在调用alice.introduce()时Python 会自动把alice作为第一个参数传给introduce但方法没有接收它的参数位置运行时就会报“takes 0 positional arguments but 1 was given”。这是面向对象入门阶段最常见的报错之一。3.3 定义属性和方法属性不一定要全部在__init__里写死。有些对象属性可以在创建后动态添加alice Person(Alice, 20) alice.email aliceexample.comPython 允许给对象动态添加新属性但这种写法可读性差也容易造成不同对象结构不一致。推荐的做法是让构造函数显式声明所有核心属性。如果某个属性允许为空可以在__init__中设置默认值class Person: def __init__(self, name, age, emailNone): self.name name self.age age self.email emailemailNone表示创建对象时可以只传姓名和年龄不传邮箱。这样做的好处是对象的结构保持一致后续代码不会因为某个对象缺属性而报错。方法设计上优先让方法只负责一类行为。比如introduce只返回介绍字符串不负责打印打印的动作交给调用方。这样方法更容易复用和测试。3.4 运行验证创建对象并调用方法验证一个类是否写对可以分三步。第一步确认类能成功创建对象alice Person(Alice, 20) print(alice)第二步确认属性能正确访问print(alice.name) print(alice.age)第三步确认方法能正确执行print(alice.introduce())如果第三步输出的方法与预期不符优先检查__init__的参数顺序、传参顺序和self是否遗漏。4. 继承让子类复用父类的属性和逻辑4.1 继承解决什么问题假设现在要通过Person类扩展出Student和Teacher。学生有学号和成绩老师有工号和课程但他们都有姓名和年龄也都需要介绍自己。如果不用继承要么把所有人塞进同一个类用类型字段区分要么完全复制Person的代码再各写一份。前者会导致一个类承担太多职责后者会导致大量重复。继承的思路是先定义父类基类Person再定义子类派生类Student和Teacher。子类自动拥有父类的属性和方法自己只写新增或需要修改的内容。通俗地说继承让子类“从父类那里获得已有的特征和行为”不需要重新造轮子。4.2 子类定义与父类调用在 Python 中定义子类时类名后面的括号里写父类名from person import Person class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id student_id def get_student_id(self): return self.student_id这里的关键是class Student(Person)表示Student继承自Person。super().__init__(name, age)调用父类的构造函数先让父类初始化name和age。self.student_id student_id再初始化子类独有的属性。super()是一个内置对象表示父类。通过它调用父类方法可以避免直接写Person.__init__带来的命名耦合。如果父类改名子类代码不需要跟着改。创建学生对象s Student(Alice, 20, 2024001) print(s.introduce()) print(s.get_student_id())输出Alice is 20 years old. 2024001可以看到Student没有定义introduce方法但依然能调用它因为它从Person继承了这个方法。4.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} (ID: {self.student_id}) is {self.age} years old.运行s Student(Alice, 20, 2024001) print(s.introduce())输出Alice (ID: 2024001) is 20 years old.这里Student重写了introduce所以调用时执行的是子类版本而不是父类版本。Python 的方法查找顺序是先找对象所属类如果这个类里没有该方法再到父类里找。这个查找机制决定了重写的规则。方法重写有两点要明确。第一重写方法的签名最好与父类保持一致也就是方法名、参数个数、返回值类型尽量不改变。这样可以避免调用方按原来的方式调用时报错。如果确实需要不同参数至少要保证旧方式仍然可用或者明确修改所有调用点。第二重写不是删除父类方法父类方法仍然存在。如果需要使用父类中已经写好的逻辑可以在子类重写的方法里用super()调用它。4.4 使用super()扩展父类行为更常见的需求是子类在父类行为基础上增加内容而不是完全替换。比如老师介绍自己时先使用父类的姓名年龄逻辑再附加课程信息class Teacher(Person): def __init__(self, name, age, course): super().__init__(name, age) self.course course def introduce(self): base super().introduce() return f{base} Teach {self.course}.运行t Teacher(Bob, 35, Python) print(t.introduce())输出Bob is 35 years old. Teach Python.这样写的好处是父类介绍逻辑发生变化时比如改成输出详细地址子类直接继承最新结果不需要手动复制一份。维护成本降低。super()不只是能调用__init__或普通方法它在多继承场景下还有更多机制但初学阶段先掌握“在子类中调用父类方法”这一个用途就足够了。4.5 多个子类的对照示例完整的person.py扩展版本可以这样组织class Person: def __init__(self, name, age): self.name name self.age age def introduce(self): return f{self.name} is {self.age} years old. 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} (ID: {self.student_id}) is {self.age} years old. class Teacher(Person): def __init__(self, name, age, course): super().__init__(name, age) self.course course def introduce(self): base super().introduce() return f{base} Teach {self.course}.使用这些类from person import Person, Student, Teacher alice Person(Alice, 20) student Student(Bob, 21, 2024002) teacher Teacher(Cathy, 35, Python) for person in [alice, student, teacher]: print(person.introduce())输出Alice is 20 years old. Bob (ID: 2024002) is 21 years old. Cathy is 35 years old. Teach Python.这段代码还体现了一个实际项目中很常见的特征多个子类对象可以放进同一个列表中都调用同一个introduce方法但各自执行不同的逻辑。这种能力叫多态后面会详细解释。5. 类层面的一些关键机制5.1 类属性与实例属性前面的示例里属性都是在__init__中通过self.xxx设置的这种属性叫实例属性。每个对象都有独立的一份修改一个对象不会影响另一个对象。类属性则不同它定义在类内部但在方法外部属于整个类本身。所有对象共享同一个类属性。class Person: species human def __init__(self, name, age): self.name name self.age age访问方式print(Person.species) p Person(Alice, 20) print(p.species)输出human human类属性适合放“所有对象都一样的常量”比如物种、组织名称、默认配置。实例属性适合放“每个对象各不相同的数据”比如姓名、年龄。需要特别注意如果在实例上给类属性名赋值实际创建了一个与类属性同名的实例属性会把原来的共享值覆盖掉。p Person(Alice, 20) p.species unknown print(p.species) # unknown print(Person.species) # human这个行为初学时容易踩坑。判断原则是读属性时先找实例再找类写属性时除非明确用类名Person.species xxx否则就创建实例属性。5.2 特殊方法__str__与__repr__直接打印对象时输出通常不友好print(Person(Alice, 20))输出__main__.Person object at 0x7f...这是因为 Python 不知道如何用字符串描述这个对象。通过实现__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})再次打印print(Person(Alice, 20))输出Person(nameAlice, age20)__str__是给用户看的可读描述__repr__是给开发者看的详细描述通常在调试时使用。如果只实现其中一个Python 在部分场景会尝试用另一个补齐。在课程学习阶段建议把__str__当作“对象自我介绍”的标准方法来实现。它在调试、日志输出、控制台展示中都非常实用。5.3isinstance、issubclass与类型检查面向对象编程里经常需要判断对象属于哪个类。Python 提供了内置函数from person import Person, Student, Teacher s Student(Bob, 21, 2024002) print(isinstance(s, Student)) # True print(isinstance(s, Person)) # True print(issubclass(Student, Person)) # Trueisinstance(s, Person)返回True是因为Student是Person的子类所以学生对象也是某种人对象。这种判断方式在编写通用处理函数时很有用。但要区分isinstance判断的是对象与类的关系issubclass判断的是类与类的关系。两者不要混用。5.4 多态的概念多态是继承带来的能力不同类型的对象响应同一个方法名但执行各自版本的行为。前面的例子中alice、student、teacher都调用introduce()内部却执行不同方法这就是多态。对调用方来说不需要关心对象具体是什么子类只需要知道它有introduce方法。这样程序可以保持稳定新增一个Staff类只要它继承Person并处理好introduce旧代码就可以直接放入新类型对象不需要修改。多态的意义是降低代码耦合让通用逻辑只依赖父类接口不依赖具体实现类型。理解这一点比记住多态这个概念本身更重要。6. 运行验证从启动新文件到观察输出6.1 一个完整可运行的示例把类定义和运行逻辑拆到两个文件保持清晰。model.py存放类demo.py存放运行逻辑。model.pyclass Animal: def __init__(self, name, sound): self.name name self.sound sound def speak(self): return f{self.name} says {self.sound} class Dog(Animal): def __init__(self, name): super().__init__(name, woof) def fetch(self): return f{self.name} fetches the ball. class Cat(Animal): def __init__(self, name): super().__init__(name, meow)demo.pyfrom model import Animal, Dog, Cat animals [ Animal(Generic, ...), Dog(Rex), Cat(Whiskers) ] for animal in animals: print(animal.speak()) rex Dog(Rex) print(rex.fetch())运行python demo.py预期输出Generic says ... Rex says woof Whiskers says meow Rex fetches the ball.这个实验验证了三条关键能力父类可以直接创建对象。子类继承父类的方法并覆盖掉默认声音。子类可以定义自己的独有方法。6.2 如何验证继承关系除了看输出还可以用代码检查继承关系是否按预期建立from model import Dog print(Dog.__mro__)__mro__是方法解析顺序Method Resolution Order会输出类查找方法时的搜索顺序。输出类似(class model.Dog, class model.Animal, class object)这个输出说明调用Dog的方法时先找Dog找不到再去Animal最后到object。object是所有类的基类。如果结果里没有预期类出现先确认类的定义顺序和继承写在哪个类名后面的括号里。6.3 练习建议学习类和继承最有价值的练习方式是做一张简单的关系表然后逐个实现。例如父类概念子类概念父类属性子类新增属性图形圆形、矩形颜色半径、宽高员工开发、测试姓名、薪资技术栈、测试类型支付微信支付、支付宝支付金额订单号先只写一个父类再写两个子类每个子类至少重写一个父类方法。跑通后再增加super()扩展行为。这种练习可以在一个小时内覆盖本讲的主要知识点。7. 常见问题与排查路径7.1 常见报错列表错误现象常见原因检查方式处理建议takes 0 positional arguments but 1 was given实例方法定义时缺少self参数查看方法定义第一参数给实例方法补上selfPerson.__init__() missing 1 required positional argument创建对象时少传参数查看调用语句和__init__参数列表对齐参数个数和顺序Student object has no attribute student_id忘记在子类__init__里初始化该属性检查子类__init__内是否设置属性补上self.student_id student_id子类调父类方法报错父类类名引用错误或导入错误检查 import 语句和类名改用super()调用父类方法打印对象显示内存地址没有实现__str__查看类中是否有__str__方法添加__str__返回可读描述AttributeError访问类属性失败类属性写到了实例方法里缩进检查把类属性移到类体内部不在方法内7.2 排查顺序遇到类和继承相关报错时建议按以下顺序排查。第一确认代码能正常导入。运行文件时如果报ModuleNotFoundError先检查文件路径、文件名、import语句是否匹配。第二确认类定义没有缩进问题。Python 通过缩进区分代码块类属性写在类体内部但不在方法内部方法内部的代码必须统一缩进。第三确认创建对象时参数与__init__参数是否对应。少传、多传、顺序错误都会引发异常。尤其要注意self不计入显式传参。第四确认super().__init__被正确调用。如果子类重写了__init__但没有调用父类__init__父类属性可能没有被初始化后续访问就会报错。第五查看完整错误堆栈。Python 报错信息会显示具体到文件的某一行这是最直接的定位线索。不要只看错误类型要看完整堆栈。7.3 继承相关的易混淆点方法重载与方法重写不同。Python 默认不提供传统意义上的方法重载同一类中后面的方法会覆盖前面的同名方法。子类中定义同名方法会覆盖父类方法这是重写。多重继承要谨慎使用。Python 支持在子类括号中写多个父类class Child(ParentA, ParentB): pass这种写法会引入方法查找顺序复杂化。初学阶段建议先掌握单继承等理解了__mro__和super()在多层查找中的行为再考虑多重继承。继承层级不要过深。实际项目里三层继承已经需要谨慎设计。每一层增加都可能带来属性来源模糊、方法覆盖难追踪、测试复杂的问题。如果发现继承层级过多优先考虑用组合代替继承。8. 类与继承的实践清单和扩展方向8.1 类设计前可以先回答这些问题设计一个类时建议先写一份小清单而不是直接编码。这个类代表什么对象名称是否清晰。对象有哪些状态这些状态用哪些属性表示。对象有哪些行为这些行为定义成哪些方法。哪些属性是对象创建时必须提供的哪些可以有默认值。哪些行为需要对外暴露哪些只适合作为内部辅助方法。这个类是否真的需要继承还是可以用组合复用。如果一个类只有属性没有方法先想一想是不是可以用字典或数据类替代。如果一个类只有方法没有属性先想一想它是不是更像工具类可以直接用模块级函数。8.2 学习环境与生产环境的差异在课程练习里类和测试写在一个文件里完全没问题运行一次丢一次也没关系。但进入真实项目后类和继承会面对更多约束。维度学习环境生产环境文件组织可以单文件建议按模块拆分类型检查弱要求建议使用类型标注日志用print使用logging继承层级1 到 2 层尽量保持浅层数据校验较少构造函数中做好校验单元测试可跳过必须覆盖核心类和方法文档可省略类注释和关键方法注释要完整生产环境中类不是“写出来能用就行”还要考虑对象生命周期、异常处理、日志记录、依赖注入和数据完整性。举例来说Person构造时如果age是负数课程练习可能不会在意但生产代码应该在__init__里校验并抛出ValueError。8.3 下一步可以扩展的方向学完类和继承后接下来可以按顺序掌握四个方向。第一个是特殊方法。学习__eq__、__lt__、__len__、__call__等它们能让自定义类更好地融入 Python 语法。比如实现__eq__后两个对象可以直接用比较内容。第二个是类型标注。在类定义和函数签名中加入类型提示class Person: def __init__(self, name: str, age: int) - None: self.name name self.age age这能显著提高代码可读性配合静态检查工具可以提前发现类型错误。MIT 6.0001 课程常用 Python 3 环境学习时可以顺手把标注习惯加上。第三个是数据类。Python 3.7 后引入dataclasses可以用更少的代码定义以数据为主的类from dataclasses import dataclass dataclass class Person: name: str age: int它会自动生成__init__、__repr__等方法适合那些纯数据载体类。但它不取代普通类行为复杂的类仍然要手动编写方法。第四个是设计模式中的组合思想。当多个类之间存在“拥有”关系而不完全是“是一种”关系时优先使用组合。比如一个Department类包含多个Employee对象而不是让Department继承Employee。组合通常比深层继承更容易测试和维护。类与继承是 Python 面向对象编程的起点也是许多框架源码、工程项目的核心组织方式。理解这一讲的关键不在于背出语法而在于能判断某个场景适合用类、怎么拆属性、怎么用继承复用逻辑、怎么在子类中安全地扩展父类行为。把最小例子反复运行几遍把错误逐个排查一遍这份基础就会在后续的项目实战中持续发挥作用。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门