ARTICLE DETAIL

资讯详情

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

Python类与继承全解析:从__init__到MRO实战指南

Python类与继承全解析:从__init__到MRO实战指南 很多初学者在接触 Python 的class时能写出简单的类也能照着文档调用别人的类但一旦遇到“继承”“方法重写”“super()”这些概念就开始迷糊了。我刚学计算机导论时也有同样的困惑为什么有了函数还要用类为什么子类可以调用父类的方法多继承到底按什么顺序找方法本文将结合 MIT 6.0001《计算机科学导论》第 9 讲的内容系统梳理 Python 类和继承的核心知识点。包括类与对象的本质、__init__与self的作用、属性和方法的设计方式、继承与重写的原理、多继承与 MRO 查找顺序以及一个完整的实战项目案例。适合正在学习 Python 基础、准备面试或想补齐面向对象编程知识点的读者。1. 类和对象到底是什么1.1 面向过程到面向对象初学 Python 时大多数人是直接从变量、函数、循环开始写的。这种“定义函数、调用函数”的写法被称为面向过程编程它的核心逻辑是“做一件事按步骤完成”。但是当程序逐渐变大比如要管理几百个学生、上千份订单或者要同时控制多个游戏角色时只用函数来组织的代码会变得难以维护。因为你需要频繁地传递很多参数而且要时刻注意哪些数据属于哪个模块。面向对象编程Object-Oriented ProgrammingOOP改变了组织方式它把“数据”和“操作数据的方法”绑定在一起封装成一个整体这个整体就是对象。对象的模板由类来定义。1.2 类就是蓝图对象就是实例我们可以用一个比喻来理解类Class像一张建筑设计图。图纸上规定了房子有几间卧室、几个卫生间、面积多大但图纸本身不是房子。对象Object是严格按照图纸盖出来的具体房子。同一张图纸可以盖出多栋户型一致但位置不同的房子。在 Python 中class就是那张蓝图而通过类创建出来的具体数据实体就是实例对象。class Student: pass s1 Student() s2 Student() print(s1) print(s2)运行结果__main__.Student object at 0x000001C5F4B9A460 __main__.Student object at 0x000001C5F4B9A490虽然这个类还没有任何内容但我们已经创建了两个独立的Student实例。每个实例在内存中有不同的地址说明它们是两个不同的对象。1.3 为什么需要类类带来的三个核心好处是封装把相关数据和函数放在一起。比如一个Dog类它有自己的name、age也有bark()方法。外部代码不需要关心内部实现细节。复用写好的类可以在多个地方使用也可以通过继承扩展新功能。抽象把真实世界的事物建模成程序中的结构让代码更贴近业务逻辑。这也是 MIT 6.0001 第 9 讲反复强调的观点类的一个核心作用是让我们能够创建“自己的数据类型”而不仅仅是使用int、str、list这些内置类型。2. 环境准备本地快速搭建 Python 学习环境在开始写代码之前先确认环境。2.1 安装 PythonMIT 6.0001 课程中使用的 Python 版本比较早但今天我们推荐直接使用 Python 3.8 以上的版本。以 3.10 或 3.11 为例语法与课程内容完全兼容且社区支持更好。官网下载地址python.org选择对应操作系统的安装包。安装时务必勾选Add Python to PATH。安装完成后打开终端或命令行输入python --version如果输出类似Python 3.11.5说明环境已经就绪。2.2 推荐的开发工具如果是纯学习用IDLEPython 自带或VS Code都可以。VS Code 需要安装 Python 扩展然后选择解释器。也可以使用 Jupyter Notebook便于逐行演示代码效果。2.3 示例项目结构本文后面的实战案例建议放在一个文件夹中python_oop_demo/ ├── animal.py ├── person.py ├── main.py如果只是跟随练习也可以直接在单个.py文件中写完。3. Python 类的基础语法详解3.1 定义一个最简单的类使用class关键字可以定义类class Dog: passpass表示空语句因为 Python 不允许空类体所以用pass占位。3.2 添加属性和方法类中可以定义两种内容属性attribute描述对象状态的变量。方法method描述对象行为的函数。class Dog: # 类属性所有实例共享 species Canis familiaris # 初始化方法创建实例时自动调用 def __init__(self, name, age): self.name name self.age age # 实例方法 def bark(self): return f{self.name} says woof! def description(self): return f{self.name} is {self.age} years old.创建对象dog1 Dog(Rex, 3) print(dog1.bark()) print(dog1.description()) print(dog1.species)输出Rex says woof! Rex is 3 years old. Canis familiaris3.3 理解__init__和self__init__是 Python 类的构造方法在创建实例时自动执行。它的第一个参数永远是self表示当前实例本身。当执行Dog(Rex, 3)时Python 实际会做两件事分配一块内存创建Dog类的实例。自动调用该实例的__init__方法把Rex和3分别绑定到name和age属性上。如果不在类中定义__init__Python 会使用默认的无参构造方法。self不是关键字你可以改成其他名字但行业惯例和 Python 官方约定使用self建议保持。3.4 实例属性和类属性的区别先看代码class Counter: # 类属性 count 0 def __init__(self): # 实例属性 self.value 0 c1 Counter() c2 Counter() print(c1.count) # 0 print(c2.count) # 0 Counter.count 1 print(c1.count) # 1 print(c2.count) # 1类属性属于类本身所有实例共享同一份。实例属性属于单个实例通过self.xxx赋值。在多数场景中会把可变状态放在实例属性中把常量性质的共享状态放在类属性中。Python 中类的真正魔力在于它允许我们把“数据”和“操作数据的函数”打包在一起而类和继承进一步让代码的组织和扩展变得更加自然这也是接下来要重点讲解的部分。4. 继承让子类复用父类的代码4.1 什么是继承继承Inheritance是面向对象编程的重要特性之一。它允许我们定义一个类子类去获取另一个类父类的属性和方法同时可以增加或修改自己的内容。继承解决的核心问题是代码复用和扩展。如果两个类有很多相同点比如Dog和Cat都有name、age都会发出叫声但叫声的内容不同我们就不应该把重复代码写两遍。正确做法是先定义一个Animal父类再让Dog和Cat继承它。class Animal: def __init__(self, name, age): self.name name self.age age def eat(self): return f{self.name} is eating. def sound(self): # 默认实现子类可以覆盖 return ...4.2 子类的基本写法class Dog(Animal): def sound(self): return f{self.name} says woof! class Cat(Animal): def sound(self): return f{self.name} says meow! dog Dog(Rex, 3) cat Cat(Whiskers, 2) print(dog.eat()) # 继承自 Animal print(dog.sound()) # Dog 自己的版本 print(cat.sound()) # Cat 自己的版本输出Rex is eating. Rex says woof! Whiskers says meow!Dog和Cat没有定义eat方法但它们从Animal中继承了它所以可以正常调用。4.3 方法重写子类可以重新定义父类已有的方法这叫做方法重写Override。在sound()的例子中Dog和Cat都重写了父类Animal.sound()方法。方法重写的好处是不同子类可以用统一接口但表现不同的行为。这种“同一个方法不同对象有不同实现”的特性就是多态Polymorphism。4.4 使用 super() 调用父类方法在子类中如果既想执行父类的逻辑又想增加自己的逻辑可以使用super()。class Puppy(Dog): def __init__(self, name, age, training_level): super().__init__(name, age) self.training_level training_levelsuper().__init__(name, age)会调用Dog父类的__init__这样name和age的初始化不用重复写。也可以在重写的方法里使用super()class Puppy(Dog): def sound(self): original super().sound() return f{original} But in a cute puppy voice!4.5 isinstance 和 issubclassPython 提供两个内置函数来判断类型关系print(isinstance(dog, Dog)) # True print(isinstance(dog, Animal)) # True因为 Dog 是 Animal 的子类 print(issubclass(Dog, Animal)) # True print(issubclass(Animal, Dog)) # Falseisinstance(obj, Class)判断一个对象是否属于指定类或其子类的实例。issubclass(Class1, Class2)判断Class1是否是Class2的子类。注意isinstance(dog, Animal)返回True因为Dog继承自Animal这也体现了“is-a”关系。5. 深入继承多继承、菱形继承与 MRO5.1 Python 支持多继承Python 允许一个类同时继承多个父类这被称为多继承。class Walker: def move(self): return walking class Swimmer: def move(self): return swimming class Amphibian(Walker, Swimmer): pass a Amphibian() print(a.move())输出walking因为Amphibian继承的父类顺序是(Walker, Swimmer)Python 会优先找Walker中的move。5.2 菱形继承问题当多个父类最终都继承自同一个基类时就形成了菱形继承结构。class Base: def who_am_i(self): return Base class Left(Base): pass class Right(Base): pass class Child(Left, Right): pass c Child() print(c.who_am_i())这里Child继承自Left和Right而Left和Right又都继承自Base。乍一看像是菱形。Python 对这种结构有专门的处理方式即MROMethod Resolution Order方法解析顺序。在 Python 3 中所有类都隐式继承自object类所以菱形结构其实非常常见。5.3 MRO 与 C3 线性化算法Python 使用 C3 线性化算法来确定方法查找顺序。可以通过类的__mro__属性查看。print(Child.__mro__)输出类似(class __main__.Child, class __main__.Left, class __main__.Right, class __main__.Base, class object)查找顺序为Child → Left → Right → Base → object。这意味着如果Child自己有who_am_i优先使用。否则查Left没有则查Right再没有查Base。最后查object。C3 算法保证三个特性子类永远在父类之前。父类保持它们在定义时的顺序。一致性如果Left在Right前面那么在 MRO 中Left永远不会出现在Right后面。这种机制保证了在多继承场景下方法查找不会陷入混乱。5.4 多继承的使用建议多继承功能强大但也容易让代码变得难以理解。工程实践中如果遇到复杂的多继承模型优先考虑使用组合而不是继承。使用混入类Mixin只提供某一个小功能名称以Mixin结尾。不要让继承层级超过三层。5.5 私有成员与名字修饰Python 没有真正意义上的私有成员。约定以单下划线_name开头的属性或方法表示“内部使用不推荐外部访问”。以双下划线__name开头的属性或方法会触发名字修饰Name Mangling实际变成_ClassName__name。class Person: def __init__(self): self.__age 18 p Person() # print(p.__age) # 报错 AttributeError print(p._Person__age) # 18名字修饰可以防止子类意外覆盖父类的“私有”属性但它不是安全机制更多是约定。6. 实战案例用类和继承实现一个宠物对战小系统为了把上面的概念串起来我们来做一个完整的 Python 项目。这个项目参考了很多初学者都喜欢的“人狗大战”练习思路但做了简化重点演示类的继承、方法重写和对象之间的交互。6.1 需求分析我们要实现一个简单的文字版宠物对战系统有Person类和Dog类。Dog继承自Animal具有名字、血量、攻击力属性。Person有名字、血量、攻击力属性可以攻击Animal。当一方的血量降到 0 或以下战斗结束。使用类方法和实例方法完善对战流程。6.2 项目结构python_oop_demo/ ├── animal.py ├── person.py └── main.py6.3 编写父类 Animal# 文件路径python_oop_demo/animal.py class Animal: 动物父类拥有基础属性和通用方法 def __init__(self, name, health, attack): self.name name self.health health self.attack attack def is_alive(self): return self.health 0 def take_damage(self, damage): self.health - damage if self.health 0: self.health 0 def attack_target(self, target): 对目标发起一次攻击 if target.is_alive(): target.take_damage(self.attack) print(f{self.name} attacks {target.name} for {self.attack} damage.) if not target.is_alive(): print(f{target.name} has been defeated!) else: print(f{target.name} is already defeated.) def __str__(self): return f{self.name}(health{self.health}, attack{self.attack})设计中尽量把通用逻辑放到父类子类只需要扩展自己的特殊行为。6.4 编写 Dog 子类# 文件路径python_oop_demo/animal.py class Dog(Animal): 狗类继承自 Animal增加狗的特殊属性和方法 def __init__(self, name, health, attack, breed): super().__init__(name, health, attack) self.breed breed def bark(self): print(f{self.name} barks: Woof! Woof!) def attack_target(self, target): 重写父类方法狗攻击时先叫一声 self.bark() super().attack_target(target)这里展示了super()的典型用法先在子类中执行额外逻辑再调用父类方法完成核心操作。6.5 编写 Person 类# 文件路径python_oop_demo/person.py class Person: 人类可以攻击动物 def __init__(self, name, health, attack): self.name name self.health health self.attack attack def is_alive(self): return self.health 0 def take_damage(self, damage): self.health - damage if self.health 0: self.health 0 def attack_target(self, target): if target.is_alive(): target.take_damage(self.attack) print(f{self.name} attacks {target.name} for {self.attack} damage.) if not target.is_alive(): print(f{target.name} has been defeated!) else: print(f{target.name} is already defeated.) def __str__(self): return f{self.name}(health{self.health}, attack{self.attack})虽然Person和Animal有相似的方法但它们是平级关系。要让代码更简洁可以继续抽取一个公共父类这里为了演示方便先保持独立。6.6 编写主程序# 文件路径python_oop_demo/main.py from animal import Dog from person import Person def battle(a, b): 两个对象互相攻击直到一方倒下 turn 0 while a.is_alive() and b.is_alive(): turn 1 print(f\n Round {turn} ) print(fA: {a}) print(fB: {b}) if turn % 2 1: a.attack_target(b) else: b.attack_target(a) print(\n Battle Over ) if a.is_alive(): print(fWinner: {a.name}) else: print(fWinner: {b.name}) if __name__ __main__: dog Dog(Rex, health80, attack15, breedLabrador) person Person(Alice, health100, attack20) print(Initial State:) print(dog) print(person) battle(dog, person)6.7 运行结果打开终端进入项目文件夹执行cd python_oop_demo python main.py输出大致如下Initial State: Rex(health80, attack15) Alice(health100, attack20) Round 1 A: Rex(health80, attack15) B: Alice(health100, attack20) Rex barks: Woof! Woof! Rex attacks Alice for 15 damage. Round 2 A: Rex(health80, attack15) B: Alice(health85, attack20) Alice attacks Rex for 20 damage. ...这个例子完整展示了类的定义与初始化。类的继承Dog继承Animal。方法重写Dog.attack_target调用了super()。对象之间的交互。实例状态的变化。6.8 进一步扩展思路添加Cat类复写不同的技能。增加武器类让Person持有一把武器对象。增加技能冷却机制。添加复活、暴击等随机事件。用异常处理来保证非法攻击不会让程序崩溃。这些扩展都可以在现有的类结构上完成不需要大改代码这就是面向对象设计的价值。7. 常见错误与排查思路学习类和继承时新手常会遇到几种典型的错误。下面整理成一份排查表。问题现象常见原因解决思路TypeError: __init__() missing 1 required positional argument创建实例时没有传足构造参数检查__init__的参数列表确保创建实例时代入所有必填参数AttributeError: Dog object has no attribute xxx调用了实例中不存在的属性通常是属性名拼写错误或属性在某个方法中未赋值检查__init__中是否初始化了该属性用dir(obj)查看对象可用属性子类调用父类方法报错没有调用super().__init__()导致父类中初始化的属性不存在在子类__init__中调用super().__init__(...)多继承时调用了错误的方法没有理解 MRO 顺序打印ClassName.__mro__查看查找顺序调整父类继承顺序NameError: name self is not defined在类外部或把self当作普通变量使用确认self只出现在类内部的方法中并且是方法的第一个参数两个对象比较总是相等或总是不等没有定义__eq__等方法按需求定义魔法方法__eq__、__lt__等recursion error在重写方法中调用super()时传入错误的类名或递归调用自身检查是否有无限递归用super()避免直接调用自身7.1 排查脚本模板当遇到不确定的问题时可以写一个小脚本快速定位obj Dog(Test, 100, 10, Poodle) # 查看对象有哪些属性和方法 print(dir(obj)) # 查看对象的类型 print(type(obj)) # 查看类型的方法解析顺序 print(Dog.__mro__) # 查看实例是否有某个属性 print(hasattr(obj, breed))dir()和hasattr()是排查属性问题时最实用的两个工具。7.2 关于“人狗大作战”类练习的提示网上有不少“人狗大作战”“宠物对战”这类练习代码实现版本很多。如果你在搜索或运行别人的代码时遇到错误不要直接复制粘贴。建议先理解它的类结构关系再对照本文提到的__init__、super()、is_alive()等关键点逐步检查。同一个需求可以有多种面向对象设计核心是理清“谁拥有什么数据谁负责什么行为”。8. 面向对象设计与工程建议8.1 命名规范类名使用大驼峰命名法MyClass、StudentInfo。方法名和属性名使用小写加下划线get_name、health。常量使用全大写加下划线MAX_HEALTH 100。私有或内部使用的属性用单下划线前缀_internal。8.2 尽量使用组合而不是继承继承不是万能的。如果两个类之间只是“使用”关系而不是严格的“is-a”关系优先使用组合。例如人类并不“继承”自枪但人类可以拥有一把枪。更好的设计是class Weapon: def __init__(self, name, damage): self.name name self.damage damage class Person: def __init__(self, name, health): self.name name self.health health self.weapon None def equip_weapon(self, weapon): self.weapon weapon def attack(self, target): if self.weapon: target.take_damage(self.weapon.damage) else: print(No weapon equipped!)组合让代码更灵活也避免了过多的继承层级。8.3 每个类应该职责单一一个类不应该同时管理数据库连接、用户界面和数据校验。拆分成小类每个类只做一件事。比如动物类只管动物的属性和动作对战逻辑可以单独放到一个Battle类中。8.4 使用抽象基类提高规范程度Python 提供了abc模块可以定义抽象基类。子类必须实现某些方法否则无法实例化。from abc import ABC, abstractmethod class Animal(ABC): abstractmethod def sound(self): pass class Dog(Animal): def sound(self): return woof # 下面这行会报错因为 Animal 是抽象类没有实现 sound # a Animal() d Dog() print(d.sound())抽象基类适合团队协作中定义统一的接口规范。8.5 异常处理在类的方法中要对不合法输入做防御。例如def take_damage(self, damage): if damage 0: raise ValueError(damage cannot be negative) self.health - damage这样可以让错误提前暴露而不是在后续计算中产生奇怪的结果。8.6 调试与日志在类中增加简单的日志可以帮助排查问题import logging logging.basicConfig(levellogging.INFO) class Animal: def take_damage(self, damage): logging.info(f{self.name} takes {damage} damage) self.health - damage在工程项目中日志比print更可靠输出可以分级、保存到文件。8.7 测试类写完后应该写单元测试。Python 自带的unittest或第三方pytest都是不错的选择。import unittest class TestDog(unittest.TestCase): def test_bark(self): dog Dog(Rex, 100, 10, Labrador) self.assertEqual(dog.bark(), ...)8.8 性能注意事项类属性和实例属性访问速度略有不同但在绝大多数业务场景中差异可以忽略。不要为了“面向对象”而把所有逻辑都塞进类简单的工具函数仍然可以用普通函数。大量创建短生命周期对象时注意内存开销。可以使用__slots__来节省内存class Point: __slots__ (x, y) def __init__(self, x, y): self.x x self.y y__slots__禁止动态创建新属性但能减少内存占用。适用于创建大量对象且属性固定的场景。9. 总结与后续学习建议这一讲的核心内容包括如何定义类、如何创建实例、__init__与self的作用、实例属性和类属性的区别、继承与重写、super()的使用方法以及多继承场景下的 MRO 查找顺序。跟随实战案例我们从零写了一个宠物对战系统实际运行并观察了对象的属性和方法如何交互。如果你已经掌握了这些内容下一步可以继续学习魔法方法__str__、__repr__、__eq__、__lt__的具体应用。抽象基类和接口设计。类方法、静态方法与实例方法的区别。正则表达式、文件读写、异常处理与面向对象结合的项目练习。尝试用类重写一个小型通讯录系统、任务管理系统或游戏角色系统巩固面向对象编程思想。可以收藏备用动手把代码敲一遍。只有亲手调试过几个报错才能真正理解类的设计逻辑。
返回列表