Python继承从入门到实战:搞定super()、MRO与多继承避坑指南

发布时间:2026/10/12 2:50:29
Python继承从入门到实战:搞定super()、MRO与多继承避坑指南 1. 继承到底帮你省了什么先从一段重复代码说起很多讲继承的文章喜欢直接扔出 class Dog(Animal) 这种例子然后告诉你“这就是继承”。但如果你只是照着敲一遍很可能第二天就忘了因为你不清楚这个冒号后面挂个父类到底解决了一个什么问题。我最早写 Python 是在一个内部管理系统里做数据导出那时候需求天天变但核心逻辑都差不多有一批对象每个对象有名字、有创建时间、要能转成字典、要能比较大小。我一开始图省事每个类都自己写一遍结果就是下面这种画面class Cat: def __init__(self, name): self.name name def to_dict(self): return {name: self.name, type: cat} def describe(self): return f一只叫{self.name}的猫 class Dog: def __init__(self, name): self.name name def to_dict(self): return {name: self.name, type: dog} def describe(self): return f一只叫{self.name}的狗这种代码你写第一遍没问题写第二遍就觉得手酸写到第三个类的时候基本已经忍不住想要复制粘贴了。复制粘贴本身没错错的是后面需求一改你很可能忘了改其中某一处。比如后来要求所有对象都加一个 owner 字段你得把 Cat、Dog、还有后续新增的 Fish、Bird 全部改一遍漏一个就是线上事故。这就是继承最朴素的动机把公共的属性和方法抽到父类里子类只保留自己的差异部分。于是上面的代码变成这样class Animal: def __init__(self, name): self.name name def to_dict(self): return {name: self.name, type: self.type} def describe(self): return f一只叫{self.name}的{self.species} class Cat(Animal): species 猫 class Dog(Animal): species 狗改动之后公有的to_dict和describe只需要维护一份每个子类用类属性species标注自己的特征。以后要加owner字段只改Animal.__init__就够了。但如果你以为继承的价值只是“少写重复代码”那你就把它的能力看小了。继承真正解决的是三个问题代码复用、统一契约、按类型分派。我们一个个说。1.1 复用只是表面契约约束才是核心复用很好理解就是父类写一次子类免费拿走。但更有价值的是“契约”。当你写class Cat(Animal)时不光是说“Cat 拥有 Animal 的代码”更是在对调用方承诺Cat 一定会有to_dict方法和name属性。这种保证是 Python 这种动态语言里非常难得的东西。你可以在代码里随时检查isinstance(cat, Animal) # True这个检查意味着什么意味着你可以写一个函数专门处理 Animal 及其所有子类对象不必关心它到底是猫还是狗def export_to_json(obj): import json if not isinstance(obj, Animal): raise TypeError(只能导出 Animal 的子类) return json.dumps(obj.to_dict(), ensure_asciiFalse)这种写法让代码具备了“面向接口”的味道我只看你要不要支持某组操作不关心你内部怎么实现。在 Python 里我们说这叫鸭子类型而在静态语言里这基本就是多态的雏形。1.2 继承与开闭原则扩展代码而不是修改代码行业里有一条设计原则叫“开闭原则”对扩展开放对修改关闭。字面有些绕说白了就是加新功能的时候尽量别动已有的、已经验证过的旧代码而是新增代码。继承是实现这个原则最顺手的方式之一。比如现在系统里已经有 Animal、Cat、Dog运营又提了个新需求加一个 Bird 类型不仅要能描述自己还要返回翼展长度。我不需要触碰 Animal 一行代码只需要新增class Bird(Animal): species 鸟 def __init__(self, name, wingspan): super().__init__(name) self.wingspan wingspan def describe(self): return f一只叫{self.name}的{self.species}翼展{self.wingspan}厘米旧代码完全不受影响新功能的入口也清清楚楚。这不是唯一正确做法但是老项目里最稳妥的做法之一。2. 基础语法与最容易被忽略的初始化细节讲语法的人很多但我发现大家最容易栽跟头的地方不是class Foo(Bar)这种写法本身而是__init__的调用问题。所以这一节我们把基础语法和初始化坑放在一起讲。2.1 定义一个父类属性和方法是怎么传给子类的先看一段最基本的代码class Vehicle: wheels 4 def __init__(self, brand): self.brand brand def start(self): print(f{self.brand} 启动了)这里Vehicle有一个类属性wheels实例属性brand以及一个方法start。接下来定义一个子类class Car(Vehicle): pass car Car(某品牌) car.start() # 某品牌 启动了 print(car.wheels) # 4你可能会发现子类明明什么都没写但brand照样被初始化了。这是因为Car没有定义自己的__init__所以它自动借用了父类的__init__。Python 找属性或方法的顺序很简单先在子类自己身上找找不到就去父类找还找不到就继续往上。这个查找顺序在单继承里非常好理解但一旦进入多继承就没那么简单了。先记着这句话后面第三章专门展开。2.2 子类重写方法的三板斧子类想要“做点不一样的事”最常见的操作是重写方法。重写有三个层次对应三种写法。第一层完全覆盖。子类不需要父类的实现自己从头写class ElectricCar(Vehicle): def start(self): print(f{self.brand} 静音启动)第二层先做父类的事再补充自己的事。这种写法一定要用super()否则父类的逻辑就被你丢掉了class ElectricCar(Vehicle): def start(self): super().start() print(电池电量剩余 80%)第三层子类新增方法父类不知道也不关心class ElectricCar(Vehicle): def charge(self): print(f{self.brand} 正在充电)这三种写法里第二层最容易出问题。新手容易忘了调super()或者写成Vehicle.start(self)虽然功能上能跑通但这样写写死了父类名称后续如果要调整继承关系就得改这里。推荐统一使用super()。2.3init到底要不要手动调两种常见写法对比这是我在同事代码里见过最多的问题。情况一子类没有自己的__init__那么父类的__init__会自动执行这个没问题。情况二子类定义了自己的__init__但又需要父类的属性那么你必须手动调用父类的__init__否则父类属性就没有被初始化。常见的错误是下面这样class Cat(Animal): def __init__(self, name, color): # 忘了调用 super().__init__(name) self.color color cat Cat(小花, 橘色) print(cat.name) # AttributeError: Cat object has no attribute name正确的写法是class Cat(Animal): def __init__(self, name, color): super().__init__(name) # 先让父类把 name 准备好 self.color color有些读者会问super().__init__(name)和Animal.__init__(self, name)有什么区别功能上几乎一样但super()不是简单地找父类它是按方法解析顺序找下一个类。在多继承场景下这两种写法会产生天壤之别。现在你只需要记住一个习惯但凡子类写了__init__第一行就写super().__init__(...)把父类的影响降到最低。3. super() 与 MRO单继承简单多继承烧脑如果你只在代码里见过单继承那你可能永远体会不到super()背后的复杂度。Python 的多继承之所以能运转靠的是一套叫做 MROMethod Resolution Order方法解析顺序的机制。这一节我们把这块硬骨头啃下来。3.1 super() 不是你爹它在 MRO 里的真实角色很多人的心理模型是super()等于父类。这个模型在单继承里基本正确但在多继承里会崩塌。先看一个经典例子class A: def say(self): print(A) super().say() class B: def say(self): print(B) super().say() class C(A, B): pass c C() c.say()请你先猜一下输出是A然后报错还是A、B实际情况是A B为什么A.say里的super().say()会调用到B.say而不是报错因为super()不是“找父类”而是“找 MRO 里的下一个类”。C的 MRO 是C - A - B - object当A在执行时MRO 的下一个就是B所以super().say()就把控制权交给了B。这就是 Python 协程式协作设计的精髓每一层方法都调用super()把调用链继续往下传直到object为止。打印出 MRO 可以看得更清楚print(C.__mro__) # (class __main__.C, class __main__.A, class __main__.B, class object)如果你在A里写死成B.say()那代码就失去了灵活性。而用super()即使以后C改成了C(B, A)调用链会自动反转代码依旧能正确执行。3.2 钻石问题C3 线性化怎么决定谁先谁后多继承里最出名的模型叫“钻石继承”四个类菱形排列。class A: def run(self): print(A.run) class B(A): def run(self): print(B.run) super().run() class C(A): def run(self): print(C.run) super().run() class D(B, C): pass d D() d.run()直觉上D继承了两个分支最终应该只跑一次A.run对吧Python 确实做到了这一点。D的 MRO 是D - B - C - A - object所以输出是B.run C.run A.run这个结果不是随机排列的它由 C3 线性化算法算出来。C3 算法有三条基本原则子类永远排在父类之前。如果多个父类出现顺序冲突按你在class D(B, C)里写的顺序为准。同一个类在 MRO 里只出现一次且后出现的会被忽略。这三条规则保证了 Python 的继承不会出现静态语言里那种“菱形歧义”。你可能不需要手算 MRO但知道这三条规则你就能解释大部分多继承结果。3.3 为什么会报 TypeError: Cannot create a consistent MRO如果父类顺序写得不对Python 在定义类的时候就会直接报错class X: pass class Y(X): pass class Z(X, Y): pass # TypeError: Cannot create a consistent MRO这个错误的意思是你要求Z的第一个父类是X但第二个父类Y本身又是X的子类。C3 算法要求“子类先于父类”可是你的写法里X在Y前面违背了这条规则。说白了父类里如果存在继承关系越底层的应该放在越前面。这个报错在写混入类的时候尤其常见。解决方式很简单调整父类声明顺序把更具体的类放在前面。4. 混入类与抽象基类更高级的继承玩法只靠基础继承写业务代码时间长了会发现一个问题单根继承的树形结构表达不了“我有某种能力”这种横向关系。比如一个类既要有日志功能又要有序列化功能还要是某个抽象类型的子类。全塞进一棵继承树里树就臃肿了。于是有两种更高级的玩法值得掌握抽象基类和混入类。4.1 抽象基类用接口约束子类而不是等运行时才报错抽象基类的存在意义不是给你复用代码的而是给你定义“这个类型必须长什么样”。它来自标准库abc用法很直接from abc import ABC, abstractmethod class Shape(ABC): abstractmethod def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius radius def area(self): return 3.14159 * self.radius ** 2如果你写一个子类没有实现area创建对象时就会直接报错class BrokenShape(Shape): pass broken BrokenShape() # TypeError: Cant instantiate abstract class BrokenShape with abstract method area这个报错发生在实例化阶段而不是等你调用area才发现。这一点在大型项目里极其有用它把“我忘了实现方法”这种低级错误从运行时提前到了构造阶段。注意抽象基类不是 Java 接口。它允许子类调用父类的具体方法也允许你通过abstractmethod强制子类实现某些方法。同时它还能配合isinstance做类型判断适合做策略模式、插件系统、协议约束等场景。4.2 混入类把能力像积木一样组合混入类Mixin是一种没有独立意义的类它专门用来给别人“混入”一组功能。通常混入类自己不会被实例化也不需要抽象方法它只是附加一组方法。看个例子class JSONMixin: def to_json(self): import json return json.dumps(self.__dict__) class LogMixin: def log(self, message): print(f[{self.__class__.__name__}] {message}) class Product(JSONMixin, LogMixin): def __init__(self, name, price): self.name name self.price price现在Product的实例既可以直接to_json()又可以log()而这两个能力彼此无关。你可以像拼积木一样把不同的 Mixin 拼到业务类上。拼的顺序还有讲究受前面说过的 MRO 影响但日常惯例是把功能简单的 Mixin 放在前面业务父类放在最后。混入类有个很重要的自我约束不要在 Mixin 里定义__init__或者至少不要覆盖父类的__init__否则容易引发不可预期的初始化链路。如果非要用初始化参数比较稳妥的做法是让 Mixin 的__init__接收*args, **kwargs然后继续往下传class JSONMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.metadata {}这样一层一层传下去最后到达真正初始化属性的类。4.3 抽象基类与混入类的组合案例抽象基类和混入类不是对立的它们可以同时出现。比如我有一个爬虫项目里面定义了数据模型基类要求所有模型实现parse方法同时混入序列化和缓存能力class BaseModel(ABC): abstractmethod def parse(self, raw_data): pass class CacheMixin: cache {} def cached_parse(self, raw_data): cache_key hash(repr(raw_data)) if cache_key in self.cache: return self.cache[cache_key] result self.parse(raw_data) self.cache[cache_key] result return result class UserModel(BaseModel, CacheMixin): def parse(self, raw_data): return {id: raw_data[id], name: raw_data[name]}继承链合并了两种能力一方面强制实现parse另一方面自动获得缓存。这比单纯复用代码要优雅得多。5. 继承的暗面什么时候千万别用继承这一节说说继承的反模式。网上经常有人说“组合优于继承”这话不完整。组合确实适合很多场景但继承也有它的不可替代之处。关键是要知道界限在哪里。5.1 继承会带入哪些隐性负担一旦你写了class B(A)B 就自动接收了 A 的所有属性和方法。好处是省事坏处是你可能根本不需要某些东西。比如 A 内部有一堆私有的辅助方法虽然名字用下划线开头但子类还是能调。时间久了子类和父类的耦合会越来越深父类一改内部结构子类可能莫名其妙报错。更隐蔽的是状态污染。父类如果有可变类属性比如一个列表或字典子类共享这份状态一不小心就改了全局。看下面这个例子class Parent: data [] class ChildA(Parent): pass class ChildB(Parent): pass ChildA.data.append(x) print(ChildB.data) # [x]子类之间共享了父类同一个列表对象。这种问题排查起来非常隐蔽因为问题不在你自己的代码里而在某个遥远的父类定义处。5.2 组合优先的评估模型我判断一个场景该用继承还是组合通常只看三件事第一有没有语义上的“is-a”关系。猫是动物圆是形状这是继承。订单有序列化能力这不算“is-a”这是“has-a”应该用组合。第二父类的方法是否大部分都适用于子类。如果子类只需要父类 20% 的功能却被迫继承了 100%继承就变味了。可以考虑把多余功能拆出去做混入类。第三是否会破坏父类的核心约束。比如父类是一个不可变的值对象子类想加可变属性那大概率不适合继承。因为调用方可能依赖父类实例的不可变性换上子类就出问题。做一个简单的判断示例class Report: def __init__(self, title, content): self.title title self.content content # 错误示范Report 有内容PaidReport 增加价格但接口里大量方法用不到 class PaidReport(Report): def __init__(self, title, content, price): super().__init__(title, content) self.price price再看组合的写法class Report: def __init__(self, title, content): self.title title self.content content class PaidReportContainer: def __init__(self, report, price): self.report report self.price price组合的版本里PaidReportContainer随时可以换掉内部的Report对象自由度更高测试也更方便。不是说要抵制继承而是说在有选择的情况下组合往往更容易维护。5.3 isinstance 的滥用与鸭子类型的冲突继承带来的一个惯性思维是想在代码里到处检查类型if isinstance(obj, Animal): ...如果滥用这个代码就会变得僵化。Python 的核心哲学是鸭子类型协议大于类型。一个对象是不是字符串不该看它继承没继承str而该看它支不支持__len__、支持不支持索引。抽象基类里提供了一种注册机制可以让一个类在不继承的情况下也被 isinstance 认定为“虚拟子类”这就是对鸭子类型的灵活补充。from abc import ABC class Runnable(ABC): pass Runnable.register(dict) print(isinstance({}, Runnable)) # True这段代码意思是字典虽然不是 Runnable 的子类但你告诉 Python 它“算” Runnable。这个机制在实现协议时很实用但也提醒我们类型判断本来就是一种耦合少用为好。6. 踩坑与调试记录七类高频问题速查继承相关的坑我踩过不少也帮别人排查过不少。这里整理一份实用速查表覆盖我实际碰过的高频问题。问题现象原因解决方式AttributeError: xxx object has no attribute name子类重写了__init__没调super().__init__()在子类__init__第一行补上父类初始化调用TypeError: Cannot create a consistent MRO父类声明顺序与继承关系冲突调整类声明顺序让更底层的类放在前面多继承调用链里同一方法执行两次某个中间类没有调用super()切断了协作链确保每个方法都用super()而非写死父类名子类修改列表父类数据也跟着变共享了父类的可变类属性改为实例属性或者在__init__里新建列表TypeError: Cant instantiate abstract class ...抽象基类的抽象方法没有完全实现补全子类里的抽象方法实现重写方法后调用父类方法报错方法签名不匹配父类方法需要参数但子类没传检查方法签名与super()调用参数__slots__子类无法设置新属性子类没有定义自己的__slots__在子类里也声明__slots__或直接避免混合使用6.1 可变类属性共享问题详解这个问题太经典单独拎出来再说两句。class Base: items [] class One(Base): pass class Two(Base): pass One.items.append(1) print(Base.items) # [1]原因很明确items是类属性所有子类引用同一个列表。修复方式是改成实例属性class Base: def __init__(self): self.items []如果设计上确实希望每个子类有独立的列表还可以在每个子类里重新定义items []但这容易遗漏不够推荐。6.2slots与继承的隐藏冲突__slots__用于限制实例属性节省内存但它和继承的配合坑很多。父类声明了__slots__子类如果也声明__slots__那么子类实例里同时有两份 slots 合并生效。但如果子类没有声明__slots__那子类实例会自动获得一个__dict__父类的 slots 限制就被打破了。class Base: __slots__ (x,) class Child(Base): __slots__ (y,) c Child() c.x 1 c.y 2 c.z 3 # AttributeError如果你的目标是内存优化那么每个子类都要显式声明__slots__这会让代码变得繁琐。所以一般项目里__slots__也不会轻易和继承混用。6.3 多继承里魔法方法为何会“消失”有朋友问过我明明父类实现了__iter__子类继承之后for循环却报错说不可以迭代。排查后发现父类里实现的是__iter__但子类继承了另一个 Mixin而那个 Mixin 里定义了__getitem__两种协议产生了依赖冲突导致 Python 走了不同的协议分支。这类问题的排查方式很简单先打印cls.__mro__逐个检查每个类里定义的方法找到被覆盖或切链的地方。多继承场景里不到万不得已不要层层覆盖魔法方法。7. 一段真实代码串起全文数据模型改造实战理论讲再多不如带大家走完一个完整的小项目。下面这个场景我虚拟过很多次一个物品登记系统最开始只有图书和杯子的数据后面要加入音频文件和打折商品同时要求所有模型支持 JSON 序列化。第一步定义父类模型class Item(ABC): def __init__(self, name, price): self.name name self.price price abstractmethod def category(self): pass def describe(self): return f物品{self.name}价格{self.price}分类{self.category()}第二步定义两个混入类class JSONMixin: def to_json(self): import json return json.dumps(self.__dict__, ensure_asciiFalse) class DiscountMixin: discount 0.9 def final_price(self): return self.price * self.discount第三步实现具体子类class Book(Item, DiscountMixin, JSONMixin): def category(self): return 图书 class Cup(Item, JSONMixin): def category(self): return 杯子 class Audio(Item, DiscountMixin, JSONMixin): def __init__(self, name, price, duration): super().__init__(name, price) self.duration duration def category(self): return 音频 def describe(self): return f音频文件{self.name}时长{duration}秒价格{self.price}第四步写一个统一的处理函数def handle_items(items): for item in items: if not isinstance(item, Item): continue print(item.describe()) print(item.to_json())现在你会发现新增一个类型只需要写一个子类定义它的category然后根据需求决定混入哪些能力。父类保证了统一接口混入类提供了横切能力具体类型只写差异化逻辑。这正是前面所有理论融在一起的落地形态。我自己在实际项目中还会在这个基础上加一个规则任何子类都尽量不要直接改父类的属性如果发现有这个需求先停下来想想是不是继承关系没设计对。通常重构一个继承结构只需要十几分钟但在错误结构上继续叠代码可能要到几个月之后才会爆发。写在最后的三个小建议聊到这儿基础语法、初始化、super、MRO、混入类、抽象类、常见坑基本都过了一遍。最后分享三个我实际写代码时的小习惯不算什么大道理但确实能减少很多问题。第一个习惯子类里凡是要调用父类方法一律用super()不要用父类类名直接调。这不是风格问题而是让代码在将来调整继承关系时不容易坏。第二个习惯写多继承之前先打印一次类.__mro__看看顺序确认哪些方法会先执行。这一步虽然简单但特别能预防那些“莫名其妙执行了另一个父类方法”的困惑。第三个习惯除非你真的需要“是一种”关系否则优先考虑组合和混入。继承不是原罪但继承用得太随意后面改起来的成本会成倍增加。我见过太多系统的祖先类里堆了几十个方法子类只用了其中两三个还时不时被父类的内部改动牵连。这种结构并不是继承的错是用的时机不对。如果这篇文章帮到了你可以试着用今天的方法回头审视一遍自己手头项目里的继承结构。不用急着大改先打印几个类的 MRO看看能不能解释清楚每个方法为什么会被调用。能解释清楚的那一刻你就算是真正摸到 Python 类继承的门道了。