ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python类与继承详解:从MIT 6.0001理解面向对象核心机制

Python类与继承详解:从MIT 6.0001理解面向对象核心机制 这次我们直接进入 MIT 6.0001 计算机科学导论的第 9 讲Python 的类与继承。这一讲是整门课程从“写脚本”转向“写系统”的分水岭。前面几讲教你怎么用变量、循环、函数组织逻辑到这一讲开始讨论如何把数据和操作数据的方法绑在一起用类去表达真实世界中的对象关系。如果你前几讲已经能写出函数式风格的程序但面对“一个对象有自己的状态和行为”这种建模需求时不知道怎么落地这一讲就是你要补的那块拼图。这篇文章我会按 MIT 6.0001 的教学节奏把类定义、实例化、继承、方法覆盖、多重继承、特殊方法这些核心知识点拆开讲每一块都给可运行的示例代码再补充实际工程里高频踩坑的排错方向。无论你是准备跟完这门课还是已经在用 Python 写项目想回头补面向对象基础都可以直接照这篇文章跑一遍。先看一下这一讲覆盖的知识点全貌再逐个击破。1. 核心知识速览知识点核心作用Python 关键字 / 语法常见误区类与对象把数据和操作数据的方法封装在一起class、__init__、self忘记self参数属性与方法描述对象状态和行为self.xxx xxx、实例方法实例变量和类变量混淆继承子类复用父类代码并扩展class A(B):继承层级过深方法覆盖子类重新实现父类方法同名方法重定义忘记调用super()super()调用父类方法协作式多重继承super().__init__()多重继承时只追求语法通过MRO方法解析顺序ClassName.__mro__不理解菱形继承特殊方法让对象支持len()、str()、等内置行为__str__、__len__、__eq__不知道这些方法是由解释器调用的从表格能看到类和继承本身的知识密度并不低但掌握它们不需要背概念核心是理解 Python 在创建对象、查找方法时到底做了什么。下面从最基本的“为什么需要类”开始。2. 类和对象的基本概念在 MIT 6.0001 前面的课程里程序基本由函数和数据组成。你定义一个列表、写几个函数处理它数据和函数是分开的。这种方式在程序规模小的时候没问题但一旦一个数据结构的形态变复杂比如要同时保存一个动物的名字、位置、饥饿程度、移动方式再用“全局变量 一组函数”去维护函数之间就会到处传递参数代码很快就会失控。类提供了一种更自然的组织方式把数据和操作数据的方法放在同一个结构里。比如描述平面上一个点直接定义一个Coordinate类让每个坐标对象自带x和y属性再提供一个计算距离的方法。这样调用方不需要关心坐标存储的细节只需要调用对象的方法。用课程里的经典表达方式来说类是模板对象是根据模板创建的实例。类里写好的属性是所有实例共有的定义但每个实例的属性值可以完全不同。理解这一点后面的继承和特殊方法才能顺利展开。class Coordinate: def __init__(self, x, y): self.x x self.y y def distance(self, other): dx self.x - other.x dy self.y - other.y return (dx ** 2 dy ** 2) ** 0.5 point_a Coordinate(3, 4) point_b Coordinate(0, 0) print(point_a.distance(point_b)) # 5.0这段代码里__init__是初始化方法在实例创建时自动执行。self代表当前实例所有实例方法的第一个参数都是它。调用point_a.distance(point_b)时Python 会自动把point_a作为self传入把point_b作为other传入。3. Python 类的定义与实例化3.1__init__方法__init__是 Python 类里最常用的特殊方法。它不能有返回值作用是在对象创建后完成初始化。这里要注意Java 或 C 中的构造器负责分配内存和初始化而 Python 中真正分配内存的是__new____init__只负责设置初始状态所以它本质上是“初始化器”。多数时候你只需要写__init__不用动__new__。课程里考察的重点是__init__的参数能不能覆盖所有必要的初始属性。如果某些属性在对象生命周期内可变也应该在__init__中显式初始化避免不同实例之间共享同一个可变默认值。class Animal: def __init__(self, name): self.name name self.hunger 50 def eat(self, food_amount): self.hunger max(0, self.hunger - food_amount)3.2 实例属性与类属性写在__init__里、通过self.xxx xxx赋值的是实例属性每个对象各有一份。直接写在class代码块顶层的是类属性所有实例共享一份。这是一个非常容易出错的细节。很多初学 Python 的人会在类顶层写一个空列表然后在某个实例里修改它结果所有实例的数据都变了。原因是类属性被所有实例共享在类外部使用instance.attr value只是给该实例新建了一个同名属性不会影响类属性。class Student: courses [] # 类属性所有实例共享 def __init__(self, name): self.name name # 实例属性每个实例独立 s1 Student(Alice) s2 Student(Bob) s1.courses.append(Python) print(s2.courses) # [Python]因为共享同一个列表如果你希望每个学生都有自己的课程列表就应该在__init__里写self.courses []。这是 MIT 6.0001 编程作业里一个高频坑点。3.3 实例方法实例方法是类中默认的方法类型。它的第一个参数是self只能通过实例调用。实例方法可以读取和修改实例属性也可以调用其他实例方法。课程中用的例子通常是“模拟动物移动”“模拟账户取款”核心都是通过方法操作对象内部状态。class BankAccount: def __init__(self, owner, balance0): self.owner owner self.balance balance def deposit(self, amount): if amount 0: self.balance amount def withdraw(self, amount): if 0 amount self.balance: self.balance - amount return amount return 0 account BankAccount(Alice, 1000) account.deposit(500) print(account.balance) # 1500实例方法的设计原则是“高内聚”。一个方法只负责一种职责这样继承时子类只需要覆盖特定行为不需要复制粘贴整套逻辑。4. 继承让子类复用父类的能力4.1 继承语法继承是第 9 讲的核心也是最容易体现面向对象设计能力的地方。语法非常直接class 子类名(父类名):。子类会自动拥有父类的属性和方法同时可以新增属性和方法也可以覆盖父类已有方法。MIT 6.0001 课程常用“动物体系”讲继承。先有一个Animal父类定义共性再派生Dog、Cat等子类表现差异。这样做的好处是公共逻辑只写一次每个子类只需要保留自己的独特部分。class Animal: def __init__(self, name): self.name name def speak(self): raise NotImplementedError(子类必须实现 speak 方法) class Dog(Animal): def speak(self): return Woof! class Cat(Animal): def speak(self): return Meow animals [Dog(Rex), Cat(Luna)] for animal in animals: print(animal.name, animal.speak())这里父类speak方法刻意抛异常目的是用“抽象接口”约束子类实现。这是一种很常见的父类设计方式虽然没有强制语法但能让你在开发时尽早发现漏实现的方法。4.2 方法覆盖 override子类里定义和父类同名的方法就是方法覆盖。调用子类实例的方法时Python 会优先查找子类的方法找不到再沿着继承链向上查找。需要注意覆盖方法时参数签名最好保持一致。虽然 Python 不像 Java 那样强制要求但如果调用方以相同方式调用参数不一致很容易埋下运行时错误。class Animal: def __init__(self, name): self.name name self.age 0 def birthday(self): self.age 1 class Dog(Animal): def __init__(self, name, breed): super().__init__(name) self.breed breed def birthday(self): super().birthday() print(f{self.name} is now {self.age} years old)4.3super()的作用super()是第 9 讲里必须掌握的方法。它返回一个代理对象用于调用父类的方法。最常见的用法是在子类的__init__中调用父类的__init__这样父类中定义的属性会自动初始化子类不用重复写一遍。如果不调用super().__init__()父类__init__里的代码不会自动执行子类就会缺少父类初始化过的属性后续访问时直接AttributeError。这是一个很典型的初学问题。class Dog(Animal): def __init__(self, name, breed): super().__init__(name) self.breed breed d Dog(Rex, Labrador) print(d.name, d.breed) # Rex Labradorsuper()不只是简单指向父类在多重继承时它的行为更复杂下一节专门展开。4.4isinstance与issubclass课程里还会讲到两个内置函数isinstance(obj, ClassName)判断对象是不是某个类或其子类的实例。issubclass(ClassA, ClassB)判断一个类是不是另一个类的子类。这两个函数在做类型检查、断言时非常有用。但实际开发中要避免滥用能用多态解决的场景就不要到处写类型判断。print(isinstance(d, Dog)) # True print(isinstance(d, Animal)) # True子类实例也是父类实例 print(issubclass(Dog, Animal)) # True5. 多重继承与 MRO5.1 为什么会遇到多重继承Python 和 Java 不同一个类可以同时继承多个父类语法是class C(A, B):。这个特性在混入类、工具类组合的场景中很有用但也会带来方法查找顺序的问题。如果只是两个毫无关系的父类多重继承不会造成困惑。真正的难点出现在菱形继承里D同时继承B和C而B和C又都继承A。此时调用一个在A中定义的方法D的实例最终应该调用哪个版本5.2 MRO 规则Python 采用 C3 线性化算法计算 MRO也就是方法解析顺序。你可以用ClassName.__mro__查看具体的解析链。class A: def who(self): print(A) class B(A): def who(self): print(B) super().who() class C(A): def who(self): print(C) super().who() class D(B, C): pass d D() d.who() # 输出 # B # C # A很多人好奇为什么D.who()会依次调用B - C - A。原因就是 MRO 把每个父类都放进了一条线性链super()在多重继承中不是“只找父类”而是沿着 MRO 找下一个符合条件的类。5.3 使用建议多重继承学习阶段能跑通示例即可生产项目里优先保持继承链简单。如果发现要用很复杂的多重继承才能复用代码更稳的方案是改用组合把一个工具类实例作为属性传入而不是硬塞进继承体系。MRO 能解释运行结果但设计上能避免则避免。6. Python 特殊方法与运算符重载6.1__str__与__repr__MIT 6.0001 第 9 讲会特别强调一个体验问题直接打印一个对象输出的是__main__.Coordinate object at 0x...根本看不出内容。想让对象在打印和调试时可读就需要实现__str__或__repr__。__str__由str(obj)或print(obj)调用面向最终用户要求可读性好。__repr__由repr(obj)调用面向开发者最好是能重建对象的表达式。class Coordinate: def __init__(self, x, y): self.x x self.y y def __repr__(self): return fCoordinate({self.x}, {self.y}) def __str__(self): return f({self.x}, {self.y}) point Coordinate(3, 4) print(point) # (3, 4) print(repr(point)) # Coordinate(3, 4)没有__repr__时调试信息里只有内存地址排查问题效率很低。这个习惯越早养成越好。6.2__eq__与__hash__两个对象用比较时默认比较的是内存地址也就是说即使内容完全相同两个实例也不相等。要让对象支持按内容比较需要实现__eq__。class Coordinate: def __init__(self, x, y): self.x x self.y y def __eq__(self, other): if not isinstance(other, Coordinate): return NotImplemented return self.x other.x and self.y other.y def __hash__(self): return hash((self.x, self.y)) p1 Coordinate(1, 2) p2 Coordinate(1, 2) print(p1 p2) # True一个容易忽略的约束是实现了__eq__后__hash__往往也要同步处理。把对象放进集合或作为字典键时Python 依赖哈希值如果两个相等的对象哈希不同就会出现非常难排查的 bug。6.3 迭代与长度协议想让自定义对象支持len(obj)、obj[i]、for x in obj这些操作分别要实现__len__、__getitem__或__iter__。这是让自定义类表现得更像内置类型的关键。class Playlist: def __init__(self, songs): self._songs songs def __len__(self): return len(self._songs) def __getitem__(self, index): return self._songs[index] playlist Playlist([A, B, C]) print(len(playlist)) # 3 print(playlist[1]) # B for song in playlist: print(song)如果实现了__getitem__且能正确处理整数索引Python 会自动用递增索引调用它来实现迭代所以连__iter__都可以先不写。当然显式实现__iter__在性能和语义上更清晰。6.4 运算符重载Python 通过__add__、__sub__、__mul__、__lt__、__gt__等特殊方法支持运算符重载。课程里的经典应用是定义一个向量类让两个向量可以直接相加而不是调用方法。class Vector: def __init__(self, x, y): self.x x self.y y def __add__(self, other): return Vector(self.x other.x, self.y other.y) def __repr__(self): return fVector({self.x}, {self.y}) v1 Vector(1, 2) v2 Vector(3, 4) print(v1 v2) # Vector(4, 6)合理重载运算符可以让代码更简洁但不要为了“炫技”把所有运算符都写一遍。只有在语义明确、调用者不会误解的情况下才值得重载。7. 类方法、静态方法与实例方法的区别7.1 三种方法的定义在类里面定义方法时默认使用self作为第一个参数这是实例方法。除了实例方法Python 还提供了classmethod修饰的类方法和staticmethod修饰的静态方法。实例方法操作实例属性需要实例才能调用。类方法第一个参数是cls操作类属性或提供备选构造器不依赖具体实例。静态方法参数与类、实例都无关只是放在类命名空间里的普通函数。class Student: major Computer Science def __init__(self, name): self.name name def introduce(self): return fMy name is {self.name}, major in {self.major} classmethod def change_major(cls, new_major): cls.major new_major staticmethod def is_valid_name(name): return bool(name) and len(name) 17.2 类方法在工程中的用途类方法最常见的用途是提供多个构造器。比如Date.from_string(2024-05-20)就能创建一个日期对象用类方法把字符串解析逻辑封装进类内部调用方不用关心解析细节。class Date: def __init__(self, year, month, day): self.year year self.month month self.day day classmethod def from_string(cls, text): year, month, day map(int, text.split(-)) return cls(year, month, day) def __repr__(self): return fDate({self.year}, {self.month}, {self.day}) d Date.from_string(2024-05-20) print(d) # Date(2024, 5, 20)7.3 三者的选择原则课程阶段可以简单记默认用实例方法需要操作类状态或提供备选构造器用类方法方法逻辑与类和实例都没有绑定关系时用静态方法。不要把所有方法都定义成静态方法否则类就退化成了一组普通函数。8. 学习类与继承时的常见误区误区后果正确做法方法定义忘记self参数调用时报参数数量错误实例方法第一个参数固定写self子类__init__不调用super().__init__()父类属性未初始化访问报AttributeError在子类__init__第一行调用super().__init__()在类顶层定义可变对象并实例间共享一个实例修改导致所有实例受影响在__init__里用self.xxx []初始化用比较两个对象却发现总是 False默认比较的是内存地址实现__eq__和__hash__继承层级过深改父类方法影响所有子类代码维护成本陡增优先用组合代替深度继承多重继承中忽略 MRO方法调用结果和预期完全不一致用ClassName.__mro__查看解析顺序这六类问题基本覆盖了初学者到进阶阶段的主要报错点。遇到问题时优先看报错堆栈再对照排查表定位比盲目改代码更有效率。9. 从 MIT 6.0001 到实际工程的扩展方向第 9 讲的内容在课程体系里只是一个起点但它的价值不在语法本身而在于让你建立面向对象的建模思维。真正进入工程后还会遇到几个紧密相关的主题建议按顺序掌握。第一个是抽象基类Python 标准库的abc模块提ABC和abstractmethod可以显式声明哪些方法是子类必须实现的。把这个和本讲里“父类方法抛NotImplementedError”的做法相对比你会体会到从约定到强制约束的演进。第二个是property装饰器。很多 Python 程序员已经习惯直接用self.attribute访问属性但有时我们需要在读取或赋值时执行校验逻辑。property可以让你在保持访问方式不变的情况下加入控制逻辑这是面向对象封装原则在实际项目中最常见的落地方式。第三个是dataclass。标准库中的dataclass能够自动生成__init__、__repr__、__eq__等方法非常适合用来保存数据的类。虽然它简化了大量模板代码但理解它生成的方法仍然要以本讲的特殊方法知识为基础。第四个是设计模式。比如工厂模式、策略模式、观察者模式它们大多数建立在类和继承之上。学完类和继承后用设计模式去重构一个之前写过的程序会明显感受到代码结构的变化。10. 总结与下一步MIT 6.0001 第 9 讲把 Python 面向对象的核心机制都过了一遍类定义、实例化、实例属性与类属性、方法、继承、方法覆盖、super()、多重继承与 MRO、特殊方法、类方法与静态方法。这些知识点单独看都不难难的是组合在一起解决问题。建议你现在就打开终端把文中所有示例按顺序跑一遍特别是super()多重继承那段手动打印D.__mro__理解为什么输出顺序是B - C - A。下一步可以给自己布置一个练习定义一个Shape父类派生Circle和Rectangle子类给它们实现计算面积的方法并实现__str__和__eq__。这个练习能覆盖本讲 80% 的核心内容跑完再进入下一讲你会觉得后面的内容轻松很多。
返回列表