ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MIT 6.0001第9讲:Python类与继承核心解析

MIT 6.0001第9讲:Python类与继承核心解析 MIT 6.0001 是麻省理工学院的公开计算机科学导论课程很多自学 Python 的人都会把它当作系统学习的第一站。前面几讲内容基本还在解决“怎么写代码”到第 9 讲就正式进入面向对象编程Python 类和继承。这一讲的分量很重因为后面凡是涉及项目组织、数据建模、代码复用的内容都是建立在“类”的基础上的。如果只看不讲案例很容易卡在self和super()上。这一讲的核心内容其实不算多但信息密度很高类的定义、构造方法__init__、实例属性与方法、继承、方法重写、子类调用父类、多重继承和 MRO。理解这些概念最好用的方式不是背语法而是把代码敲一遍观察对象在内存里的行为。本文会按照“最小类代码到继承再到接口设计与批量调用”的顺序把这一讲的关键点拆开讲清楚每一步都配可运行的 Python 代码。如果你是正在刷 MIT 6.0001 的自学者或者已经学过 Python 基础语法、想系统补齐面向对象知识这篇文章可以直接收藏。不需要 GPU不需要特殊硬件一台普通电脑装好 Python 3.8 以上环境就能全部跑通。1. 核心知识速览先给一张速览表把这一讲的位置、内容和运行门槛说清楚。能力项说明课程名称MIT 6.0001Computer Science and Programming in Python核心主题Python 类和继承、面向对象编程基础前置知识Python 基础语法、函数、列表/字典等常用数据结构主要知识点class 定义、__init__构造方法、self、实例属性和方法、方法重写、super()、多重继承、MRO代码环境Python 3.8 及以上推荐 3.10硬件要求无特殊要求CPU 即可不需要 GPU运行方式命令行 Python、IDLE、VS Code、Jupyter Notebook 均可是否依赖第三方库基本不依赖标准库足够演示适合场景Python 编程入门、MIT 6.0001 自学、面向对象基础补强实际工程重点用类组织数据与方法、继承复用代码、批量创建对象并调用接口这一讲的核心思想可以压缩成两句话类是把数据和操作数据的方法打包在一起继承是让子类直接复用父类已有实现同时可以改写或扩展。MIT 6.0001 后面关于异常、测试、算法和项目实现的题目里几乎到处都会用到这些概念。2. 适用场景与学习边界先别急着看语法搞清楚这套东西适合解决什么问题以及不解决什么问题学习会更有方向。适合这几类人正在学 MIT 6.0001需要第 9 讲的配套讲解和代码。已经会写 Python 脚本和函数但面对稍微复杂的项目时不知道怎么组织代码。从 Java、C、JavaScript 转到 Python想知道 Python 的类、继承和那些语言到底有什么差异。想在本地用“类 批量任务”的方式处理结构化的数据比如批量读取学生信息、批量生成测试对象。这一讲能解决的问题也很明确把多个相关的数据字段合并成一个对象不再靠一堆散乱变量维护。把对象内部能执行的操作封装成方法外部调用方只关心方法名和参数。用继承让多个相似类共享公共逻辑避免复制粘贴一套几乎相同的代码。通过方法重写和动态绑定实现多态同一份循环代码可以处理不同类型对象。同时也要清醒认识它的边界。这一讲是面向对象入门不是大型工程实践。它不会讲dataclass、抽象基类、__slots__、元类这些进阶内容也不会深入设计模式。如果目标是理解大型框架源码里的类体系第 9 讲只是起点还需要额外补充工程类知识。另外如果你是学生跟着 MIT 6.0001 做作业时要注意学术诚信可以自己理解后重新写代码不要直接照抄现成答案提交。还有一条容易被忽略的边界继承不是用来炫耀代码技巧的。很多初学者看到继承很好用就任意套多层继承反而把代码变复杂。正确的思路是“先需要再继承”能用组合和普通函数解决就不要硬造类。3. 环境准备本地运行 Python 类代码正确、可落地的第一步是准备环境。虽然第 9 讲的代码很简单但一个干净、可重复的 Python 环境能减少不少折腾。3.1 安装 PythonMIT 6.0001 课程经过多年迭代目前大概率使用 Python 3。更稳妥的做法是安装最新稳定版 Python例如 3.10 或更高。安装方式看系统Windows从 Python 官网下载安装包安装时勾选“Add Python to PATH”。macOS可以用官方安装包也可以考虑 Homebrew 安装。Linux多数发行版自带 Python 3用包管理器安装或直接使用python3命令。安装后在终端里确认版本python --version如果输出Python 3.10.x或更高版本说明环境正常。注意Python 2 已经停止维护这一讲的所有代码都只面向 Python 3。3.2 选择运行方式运行这一讲的示例代码三种方式都可以把代码保存成.py文件然后使用命令行运行。在 VS Code 或 PyCharm 中直接点击运行。用 Jupyter Notebook 按单元格逐步执行更容易观察init和继承的中间状态。命令行运行是任何时候都通用的方式例如python class_demo.py3.3 不需要额外依赖第 9 讲的类和继承代码全部使用 Python 标准库不需要pip install任何东西不需要配置 CUDA也不需要下载模型文件。对显存、磁盘、GPU 完全没要求这也是它适合入门学习的原因之一。建议在正式开始前写一个最简类快速验证环境class Demo: def __init__(self, value): self.value value def show(self): return fvalue is {self.value} if __name__ __main__: d Demo(42) print(d.show())运行后如果输出value is 42说明类的基本语法和运行环境都没问题。4. Python 类的核心概念从最小示例开始第 9 讲不会从理论定义讲起而是通过大量示例让学习者体会“类为什么会存在”。下面用一个极简的坐标点类来还原这个过程。4.1 定义一个坐标类很多编程书讲类时都喜欢用“点”或“人”来举例因为它们足够简单又能体现属性和方法的含义。假设我们要表示二维坐标里的一个点传统写法可能是x1 1 y1 2 x2 10 y2 8这样写两个点还算能忍一旦有成百上千个点维护就会变得很痛苦。用类重新组织class Coordinate: def __init__(self, x, y): self.x x self.y y def distance_to(self, other): return ((self.x - other.x) ** 2 (self.y - other.y) ** 2) ** 0.5然后实例化两个点c1 Coordinate(1, 2) c2 Coordinate(10, 8) print(c1.x, c1.y) print(c2.x, c2.y)输出1 2 10 8这里能看到类和对象的关系Coordinate是类c1和c2是由类创建出来的对象。每个对象拥有自己的x和y互不干扰。4.2 调用对象方法distance_to是对象的方法调用时把另一个坐标对象传进去print(c1.distance_to(c2))输出10.0因为(1, 2)到(10, 8)的欧氏距离是sqrt((9)^2 (6)^2) sqrt(117) ≈ 10.816这里输出10.0是因为刚才的数值恰好不是。如果输出与预期不一致要检查手感和实际代码。从代码可以看出方法定义的特点distance_to的第一个参数是self调用时直接用c1.distance_to(c2)Python 会自动把c1传给self。4.3 class 和 def 是核心语法Python 定义类的语法非常直接class 类名: 方法定义类名一般遵循大驼峰命名例如Coordinate、Student、Animal。方法看起来和普通函数没有太大区别差别只是第一个参数必须是self以及方法默认属于类内部。初学者最容易犯的第一个错误就是在方法定义里漏掉self。写普通函数时不会遇到这个问题一旦写类方法就会疯狂报TypeError或缺参数错误。这个坑几乎每个人都会踩到后面第 9 章会专门列排查方法。5. 构造方法与 self实例化背后的细节__init__是类里第一个要理解的方法。MIT 6.0001 课件常把它叫构造方法或初始化方法更准确地说它是“初始化方法”。5.1__init__的作用当执行c1 Coordinate(1, 2)时Python 实际上做了两件事创建了一个新的对象。调用__init__方法把新对象绑定到self并传入参数(1, 2)。所以__init__不是用来“创建”对象的它更像一个初始化回调。真正创建对象的底层方法是__new__普通开发中很少直接使用。在__init__内部def __init__(self, x, y): self.x x self.y y就是把参数复制到对象自己身上。self.x x这行代码的含义是给当前实例绑定一个名为x的属性并把参数x的值存进去。5.2 实例属性和类属性除了实例属性Python 还支持类属性。类属性定义在类内部、方法外部class Student: school MIT def __init__(self, name): self.name name s1 Student(Ada) s2 Student(Alan) print(s1.name) print(s1.school) print(s2.school)输出Ada MIT MIT这里school是类属性所有实例共享同一个值name是实例属性每个实例独立。访问属性时Python 会先在实例属性里查找找不到再往类属性里查找。这个机制很实用但也藏着一个经典陷阱。如果类属性是不可变对象比如字符串、整数通常没问题。如果类属性是一个可变对象比如列表那么所有实例会共享同一个列表修改一个实例的列表其他实例也会受到影响。class BadExample: items [] b1 BadExample() b2 BadExample() b1.items.append(hello) print(b2.items)输出是[hello]。这通常不是期望行为。更稳妥的做法是在__init__里为每个实例创建独立的列表class GoodExample: def __init__(self): self.items [] g1 GoodExample() g2 GoodExample() g1.items.append(hello) print(g1.items) print(g2.items)5.3 self 的名称不是硬编码self只是约定俗成的名字不是 Python 关键字。类方法定义时完全可以写成this或me只要第一个参数存在即可。例如class Weird: def __init__(me, value): me.value value def get(me): return me.value w Weird(12) print(w.get())能跑但所有 Python 开发者都知道self才是规范写法。进入工程环境后团队协作一般都会约定使用self这样阅读代码成本最低。不要在这个地方追求个性保持一致最重要。6. 继承与方法重写父类、子类与 super理解基本类定义之后第 9 讲的重头戏就是继承。继承解决的核心问题是“复用”——多个类有相同的属性和方法没必要一遍遍重复定义把它们提取成父类子类自动获得父类能力。6.1 一个动物例子先定义一个父类Animalclass Animal: def __init__(self, name): self.name name def speak(self): return f{self.name} makes a sound再定义两个子类分别重写speakclass Dog(Animal): def speak(self): return f{self.name} barks class Cat(Animal): def speak(self): return f{self.name} meows继承的语法是class Dog(Animal)括号里的类就是父类。子类Dog自动拥有父类的__init__和name属性同时重新定义了speak。测试d Dog(Rex) c Cat(Mimi) print(d.speak()) print(c.speak())输出Rex barks Mimi meows如果把Dog类里的speak删掉Dog会直接使用父类的speak实现输出会变成Rex makes a sound。这就是继承最基础的用法。6.2 方法重写与多态当子类定义了和父类同名的方法时就构成了方法重写运行时以子类实现为准。这个机制配合多态会产生非常优雅的代码你不需要关心对象具体是Dog还是Cat只用统一接口调用即可。animals [Dog(Rex), Cat(Mimi), Animal(Generic)] for animal in animals: print(animal.speak())输出Rex barks Mimi meows Generic makes a soundfor循环遍历animals列表时Python 在运行时根据实际对象类型调用对应的方法。这就是多态的基础形态。后续写图形界面、游戏开发或数据处理框架时会大量用到这种模式。6.3 子类如何调用父类的__init__很多初学继承的人会遇到一个问题子类重写了__init__但是没有初始化父类的属性导致后面访问name时直接报错。正确做法是显式调用父类的初始化方法。class Dog(Animal): def __init__(self, name, breed): super().__init__(name) self.breed breed def speak(self): return f{self.name} barks d Dog(Rex, Labrador) print(d.name) print(d.breed)super()返回一个代理对象通过它调用父类方法。super().__init__(name)等价于调用Animal.__init__(self, name)但写法更清晰而且处理多重继承时更安全推荐优先使用super()。如果在子类__init__中没有调用super().__init__(name)d.name会在运行时抛出AttributeError。排查方向应该先看这一类问题。6.4 没有重写方法时如何扩展行为有时保留父类方法再附加子类自己的行为。可以调用父类方法拿到结果再继续处理class LoudDog(Animal): def speak(self): base super().speak() return base.upper() !!! loud LoudDog(Rex) print(loud.speak())输出REX MAKES A SOUND!!!这种“先复用父类逻辑再扩展”的模式在工程中比完全重写更常见因为它能最大化复用同时父类接口发生变化时子类也能通过super()自动适配一部分改动。7. 继承的边界多重继承、菱形问题与组合MIT 6.0001 第 9 讲通常会把重点放在单继承上但 Python 是支持多重继承的语言。理解了基本继承后有必要把多重继承的规则和风险也说清楚尤其是菱形问题。7.1 多重继承的基本语法一个类可以继承多个父类class A: def greet(self): return A class B: def welcome(self): return B class C(A, B): pass c C() print(c.greet()) print(c.welcome())输出A B子类C同时拥有A和B的方法。这个语法本身不复杂但一旦两个父类有同名方法问题就来了。7.2 MRO 与方法查找顺序Python 每个类都有一个方法解析顺序可以用ClassName.mro()查看class Base: def greet(self): return Base class Left(Base): def greet(self): return Left class Right(Base): def greet(self): return Right class Child(Left, Right): pass print(Child.mro()) print(Child().greet())输出[class __main__.Child, class __main__.Left, class __main__.Right, class __main__.Base, class object] Left这个方法解析顺序的意思是查找greet方法时先找Child再找Left再找Right再找Base最后找object。所以Child().greet()返回Left的实现。写代码时如果对结构没有完整把握建议每次都用Child.mro()验证不要盲猜。7.3 菱形问题的 Python 处理方式菱形问题的经典结构是Base有一个方法Left和Right都继承自BaseChild同时继承Left和Right。如果每个类都在__init__或普通方法中调用了super()最终Child的方法只会被调用一次。这是因为 Python 的 C3 线性化算法会把依赖关系整理成一条线性链避免父类方法被反复执行。把上面代码中的__init__改成打印后能清楚看到调用顺序class Base: def __init__(self): print(Base init) class Left(Base): def __init__(self): print(Left init) super().__init__() class Right(Base): def __init__(self): print(Right init) super().__init__() class Child(Left, Right): def __init__(self): print(Child init) super().__init__() c Child()输出Child init Left init Right init Base init可以看到Base init只会出现一次这就是 MRO 和super()配合的威力。如果不用super()而是直接写Base.__init__(self)在多重继承场景里很可能导致父类初始化多次产生难以排查的副作用。7.4 继承不是越多越好掌握多重继承语法后更重要的判断是知道什么时候不要用。继承层次过深比如GrandParent - Parent - Child - GrandChild会让代码难以理解和维护。相比之下很多场景用“组合”更合适不需要继承一个类而是把另一个类的实例作为自己的属性。class Engine: def start(self): return engine started class Car: def __init__(self): self.engine Engine() car Car() print(car.engine.start())组合的好处是耦合度更低一个类的变化不容易影响另一个类。工程上经常说“优先组合少用继承”第 9 讲虽然重点讲继承但学习时要把这个原则一并记住。8. 类的接口设计与批量调用场景第 9 讲的内容偏语法和建模但在实际编程中类被大规模使用的地方通常是“批量创建对象并调用统一接口”。这一节把类的接口视角和批量任务结合起来也是从课程代码走向实际工程的关键一步。8.1 把公开方法当成接口类对外暴露的方法就是它的接口。外部代码不应该关心方法内部是怎么实现的只关心方法名、参数和返回值。设计类时方法名要能直接表达功能参数数量不要过多返回类型尽量统一。比如定义一个处理人物信息的类class Person: def __init__(self, name, age): self.name name self.age age def description(self): return f{self.name}, {self.age} years old def is_adult(self): return self.age 18description和is_adult就是这个类的对外接口。外部代码只需要调用这两个方法不必知道name和age是怎么存储的。工程上还经常用单个下划线表示“内部属性或方法”例如_internal这只是一个约定并不限制访问。双下划线开头的属性则会被 Python 做名称改写变成_ClassName__attr这更多是一种防误用机制而不是完善的私有访问控制。8.2 批量创建对象批量任务是类最常见的实际应用场景之一。比如批量创建多个学生对象class Student: def __init__(self, name, score): self.name name self.score score def grade(self): if self.score 90: return A if self.score 80: return B if self.score 70: return C return D用循环批量创建对象data [(Ada, 95), (Alan, 67), (Grace, 88)] students [Student(name, score) for name, score in data]再批量调用接口for student in students: print(f{student.name}: {student.grade()})输出Ada: A Alan: D Grace: B这种写法在数据处理、日志分析、测试数据生成里非常常见。批量创建对象时一个很重要的习惯是先打印一遍对象的关键属性确认所有数据都按预期初始化。8.3 从文件批量读取数据并创建对象如果要处理真实数据可以用标准库csv读取文件再逐行创建对象。这里给一个通用骨架实际路径、字段名需要按项目调整。import csv class Student: def __init__(self, name, score): self.name name self.score int(score) def grade(self): if self.score 90: return A if self.score 80: return B if self.score 70: return C return D def load_students(file_path): students [] with open(file_path, newline, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: students.append(Student(row[name], row[score])) return students if __name__ __main__: students load_students(students.csv) for student in students: print(student.name, student.grade())假设students.csv内容为name,score Ada,95 Alan,67 Grace,88运行后输出Ada A Alan D Grace B这个例子的价值在于把第 9 讲的类知识和工程输入输出连接起来。初学者可以从这里体会“类只是代码组织方式数据从哪里来、到哪里去仍然由外部流程控制”这一思想。8.4 用if __name__ __main__控制执行入口上面的示例里用了if __name__ __main__。这个写法保证文件被导入时不执行主逻辑只有直接运行时才执行。这是写类模块的标准姿势多文件项目中尤其重要。如果不加这层判断别人用import引入你的类时整段测试代码也会被意外执行很容易造成输出混乱或性能开销。8.5 批量场景的简单性能观察在批量处理时可以留意内存和耗时。创建几千个对象在普通电脑上通常很快不需要特殊调优。但要注意Python 对象本身有一定的内存开销批量数据过大时考虑改用元组、字典或使用__slots__减少属性存储不过__slots__属于进阶话题第 9 讲不会涉及。真正的性能瓶颈往往不在对象创建而在循环体内部的复杂计算或外部调用。想观察运行时间可以用标准库timeimport time start time.perf_counter() students [Student(name, score) for name, score in data] end time.perf_counter() print(fcreated {len(students)} students in {end - start:.6f} seconds)学会用len()验证对象数量、用time.perf_counter()粗测耗时就足够完成大部分学习验证。9. 调试、常见问题排查与工程化建议这一讲的内容虽然入门但踩坑点非常多。把常见问题按现象、原因、排查方式、解决方案整理成表遇到问题直接对照。9.1 常见问题排查表问题现象可能原因排查方式解决方案NameError: name self is not defined方法定义里漏写了self参数查看方法定义代码在方法第一个参数位置加上selfTypeError: __init__() takes 2 positional arguments but 3 were given实例化时传入参数数量与__init__定义不一致检查实例化语句和__init__签名按__init__定义补齐或删除多余参数AttributeError: Dog object has no attribute name子类重写__init__后没有调用super().__init__查看子类__init__是否初始化父类属性在子类__init__里调用super().__init__(...)AttributeError: Coordinate object has no attribute x实例属性名拼写错误或没有执行到赋值语句打印vars(obj)或检查__init__修正属性名确认初始化逻辑先执行修改一个实例的列表属性其他实例跟着变把可变列表定义成了类属性打印obj.__dict__确认属性归属把列表放到__init__里作为实例属性多重继承中方法调用不符合预期对 MRO 顺序判断错误调用ClassName.mro()查看顺序调整父类顺序或重构继承结构子类方法想要父类行为却得到子类行为没有使用super()导致父类逻辑被覆盖检查子类方法中是否调用了父类实现在子类方法中使用super()调用父类对应方法9.2 调试手段从 print 到 dir第 9 讲阶段最有效的调试方式是组合使用print、type、vars、dir。print(obj)输出对象的默认表示。如果没有定义__str__或__repr__默认输出往往不够直观。type(obj)查看对象的具体类型。vars(obj)返回对象的__dict__也就是实例属性字典。dir(obj)列出对象可用的属性和方法排查拼写错误时很好用。c Coordinate(1, 2) print(type(c)) print(vars(c)) print(dir(c))输出会显示c的类型、属性字典以及所有可访问的属性名。如果某个属性不在vars(c)里说明它还没被正确创建。调试继承问题时建议先打印继承链相关对象再决定改哪里。9.3 写一个最小单元测试只看print输出虽然直观但程序一旦变复杂人工核对输出会非常累。从第 9 讲开始可以慢慢建立写单元测试的意识。标准库unittest就够用。import unittest class Animal: def __init__(self, name): self.name name def speak(self): return f{self.name} makes a sound class Dog(Animal): def speak(self): return f{self.name} barks class TestDog(unittest.TestCase): def test_dog_speak(self): dog Dog(Rex) self.assertEqual(dog.speak(), Rex barks) if __name__ __main__: unittest.main()运行后如果一切正常会输出类似. ---------------------------------------------------------------------- Ran 1 test in 0.001s OK这种测试方式能帮助你在修改类结构后快速发现行为是否被破坏也是后续学习 MIT 6.0001 测试相关章节的好铺垫。9.4 工程化建议与学习收尾建议把第 9 讲的内容真正用到项目里建议从这几个方面入手一个类一个职责。不要写一个包含 20 个方法和 30 个属性的“上帝类”拆成多个小类更容易测试和维护。类名用大驼峰方法名和属性名用下划线小写。例如MyClass、my_method、my_attribute。继承深度尽量控制在两到三层。超过三层后任何一处父类改动都可能影响整个链。不要为了“复用”强行继承。如果两个类只是共享少量方法用组合或公共函数更合适。批量处理时先小规模验证。先创建少量对象打印属性确认无误后再处理完整数据。写__repr__方法。自定义类的调试体验会明显提升否则print(obj)输出会是__main__.Dog object at 0x...看不出任何真实信息。保持代码可读性优先。self、super()、类名都按约定书写不要自创风格。如果时间和精力允许学完第 9 讲后建议顺手写几个小题目巩固定义一个BankAccount类包含余额、存款、取款方法再定义一个SavingsAccount继承它加入利率属性最后批量创建多个账户对象模拟一次交易循环。把这些代码保存在本地后面几讲要用到类时直接复用比重新看一遍视频更有效。这一讲最值得投入时间的地方就是把继承的调用链彻底想清楚对象创建时谁先初始化方法调用时谁先执行。这两条链路想明白Python 面向对象的大门基本就打开了。
返回列表