第13篇|面向对象二:继承、多态、MRO
本篇定位:《Python 教程》专栏一「Python 从零入门」的第 13 篇。承接第 12 篇「类、实例、属性、方法」——你已经能把数据与行为收进类型;本篇把「一种东西的变体」讲透:子类、方法重写、
super()、多态,以及决定属性继续往哪找的 MRO(Method Resolution Order)。为第 14 篇魔术方法与数据模型、第 18 篇类型注解 /dataclass、第 28 篇最小 Agent、第 40 篇nn.Module打桥。读完你将能够:用单继承表达「共享接口 + 特化行为」;正确重写方法并用
super()协作初始化;用isinstance/issubclass做类型判断;区分鸭子类型与名义继承下的多态;读懂Class.mro与菱形继承的线性序;用 Mixin /abc.ABC做轻量组合与接口契约;在 AI Infra / Agent 场景里设计 Tool 层次与配置子类。预计用时:阅读 35~45 分钟,动手 25~35 分钟。
第 12 篇末尾的 EchoTool(Tool) 只是预告片:父类约定 name / run,子类填一种实现。真实项目里工具会越来越多——检索、计算器、HTTP 调用、读文件——若每个都从零复制字段与校验,改接口时要改 N 处。继承解决的是「共享一份约定,各自特化行为」;多态让调用方写 tool.run(query) 而不用写一长串 if;MRO 则回答第 12 篇属性查找图延长之后的问题:找不到时,按什么顺序继续找?
和第 02 篇会合:子类实例的 type 是子类,但仍「算作」父类的一种(isinstance)。和第 07、08 篇会合:重写的方法仍是函数,经实例绑定;super() 拿到的是按 MRO 前进的代理,不是魔术地“跳到字面父类”。和第 11 篇会合:class ConfigError(Exception) 本身就是继承——异常树靠同一套机制铺开。下一篇(14)会讲 str / eq / iter 等协议;本篇先把“类型之间的关系”立住。
建议边读边开练习目录(uv init --no-package)。阅读顺序:为何继承 → 单继承与重写 → super() → 多态与鸭子类型 → MRO / 菱形 → Mixin 与 ABC / AI Infra。踩坑大全请过一遍,尤其是「忘调 super().init」「用 type(x) == 堵死子类」「菱形里手写父类名而非 super」。
也可以先用一句话定心:子类扩展或替换父类行为;查找按 MRO;同一接口下不同实现叫多态。 本篇把这句话拆开。
动手时建议准备两份对照:一份继续用第 12 篇的单类 Tool + 多个互不相关的实现函数;另一份改成 Tool 基类与若干子类,用同一个 dict[str, Tool] 调度。对比“加一种新工具要改几处”“调用方要不要写 if name == ...”。这种对比比抽象讨论“该不该继承”更有体感。
本篇会写到多重继承与 ABC,但主线仍是单继承。许多生产代码一辈子只需要“一个基类 + 若干子类”;Mixin 与菱形是为了你读框架源码时不懵,不是鼓励一上来就设计五条继承边。和“全用字典 + 一堆函数”相比,继承带来的变化是:扩展点有名字、有类型、有查找规则——弄清规则,比背 UML 术语更划算。
和第 09 篇合在一起看:基类与子类常常住在同一模块或同一包的不同模块里,用 from tools import EchoTool 导入。注册表收集的是实例,类型检查用的是类。训练脚本里的 ChatConfig(ModelConfig)、推理服务里的后端插件类,几乎都是「模块里的继承树 + 运行时的多态实例」。
一、为什么需要继承:共享接口 + 特化行为
1.1 复制粘贴的上限
没有继承时,你可能为每种工具各写一个完整类,字段与校验重复:
class EchoTool:
def __init__(self, name: str, description: str):
self.name = name
self.description = description
def run(self, query: str) -> str:
return f"echo: {query}"
class SearchTool:
def __init__(self, name: str, description: str, top_k: int = 3):
self.name = name
self.description = description
self.top_k = top_k
def run(self, query: str) -> str:
return f"hits[{self.top_k}]: {query}"
能跑,但 name / description 的约定散落两处;日后要加 timeout 或统一日志,就要改每一个类。Agent 注册表想声明「所有工具都是 Tool」,也缺少共同的类型名。
1.2 继承:蓝图上的特化
继承(inheritance) 让子类(subclass)自动获得父类(base / superclass)的属性与方法,再按需重写(override)或追加。调用方可以依赖父类接口,运行时放入不同子类实例——这就是下一节要展开的多态。

图1 继承树:基类 Tool → 子类特化
class Tool:
def __init__(self, name: str, description: str):
self.name = name
self.description = description
def run(self, query: str) -> str:
raise NotImplementedError("subclass must implement run")
class EchoTool(Tool):
def run(self, query: str) -> str:
return f"echo: {query}"
class SearchTool(Tool):
def __init__(self, name: str, description: str, top_k: int = 3):
super().__init__(name, description)
self.top_k = top_k
def run(self, query: str) -> str:
return f"hits[{self.top_k}]: {query}"
echo = EchoTool("echo", "echo the query")
search = SearchTool("search", "toy search", top_k=5)
print(echo.name, echo.run("ping"))
print(search.name, search.run("agent"))
echo echo: ping
search hits[5]: agent
EchoTool 没写 init,创建时直接用父类的;SearchTool 有额外字段,就用 super().init(...) 先让父类填好共享部分(第三节细讲)。
AI Infra 视角:LangChain / 自研 Agent 的 Tool、PyTorch 的
nn.Module子类、推理框架里的ModelConfig变体,几乎都是「基类定契约,子类填实现」。先练好本篇的树与查找规则,读源码时才分得清“继承来的默认行为”和“本类覆盖的钩子”。
1.3 组合仍然重要
第 12 篇的 CallableTool(持有一个函数)提醒我们:不是所有复用都要靠继承。共享大量实现、偶尔覆盖 → 继承合适;行为热插拔、只有一个 run 委托 → 组合往往更短。本篇把继承刀法练熟,不等于凡事都造五层继承树。
经验法则可以记成两句:
- 是一种(is-a)且要共享实现或接口名 → 考虑继承(
EchoTool是一种Tool)。 - 有一个(has-a)可替换部件 → 考虑组合(
Tool里持有http_client,而不是继承HttpClient)。
团队评审里若出现“为了复用两行代码就抽基类”,往往组合或模块级函数更干净。继承的成本是:耦合了类型关系——父类改签名,所有子类与 isinstance 分支都可能受影响。把刀用在真正的扩展点上。
再补一句和测试有关的:子类越多,越要用“针对接口的测试”(给定任意 Tool,run 返回字符串、不抛未声明异常)加上“针对特化的测试”(SearchTool 的 top_k)。第 22 篇 pytest 会回到这点;本篇先把对象形状定对。
二、单继承语法、重写与类型判断
2.1 语法与「是一种」
class Base:
pass
class Sub(Base):
pass
print(issubclass(Sub, Base))
True
表示 Sub 是一种 Base(is-a)。子类实例拥有父类方法;子类里定义的同名方法会遮盖父类版本——查找在子类命中后通常不再往上(除非你显式 super())。
class Animal:
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "woof"
d = Dog()
print(d.speak())
print(isinstance(d, Dog), isinstance(d, Animal))
print(issubclass(Dog, Animal), issubclass(Dog, Dog))
woof
True True
True True
2.2 isinstance / issubclass
| API | 含义 | 注意 |
|---|---|---|
| isinstance(obj, Cls) | obj 是否为 Cls 或其子类的实例 | 可传元组:isinstance(x, (int, str)) |
| issubclass(Sub, Base) | Sub 是否为 Base 的子类(含自身) | 参数必须是类,不要传实例 |
第 12 篇踩坑已预告:用 type(x) == SomeClass 做分支会拒绝合法子类。业务判断优先 isinstance。
class Tool:
pass
class EchoTool(Tool):
pass
t = EchoTool()
print(type(t) == Tool) # False:精确类型不是 Tool
print(isinstance(t, Tool)) # True:子类算数
False
True
2.3 重写与扩展
重写可以完全替换父类行为,也可以先做自己的事再(或不)调用父类逻辑。扩展字段时,别忘了初始化链——见图 2。

图2 方法重写与 super() 协作链
class Tool:
def __init__(self, name: str):
self.name = name
def info(self) -> str:
return f"tool={self.name}"
class TimedTool(Tool):
def __init__(self, name: str, timeout: float):
super().__init__(name)
self.timeout = timeout
def info(self) -> str:
base = super().info()
return f"{base}, timeout={self.timeout}"
print(TimedTool("search", 1.5).info())
tool=search, timeout=1.5
重写时有两个常见意图,写代码前先选清:
- 替换:子类行为与父类完全不同(
EchoTool.run不再调用父类的NotImplementedError版本)。 - 扩展:先/后插入逻辑,中间仍复用父类(
info例子用super().info())。
调试时若不确定“到底跑的是哪一版方法”,可以临时在子类与父类各打一行日志,或直接 print(Sub.run, Base.run) 看是否为不同函数对象。第 12 篇的方法绑定依然成立:通过实例取出的是绑定方法,func 指向查找命中的那一份函数。
三、super() 与协作式继承
3.1 super() 不是“父类的别名”
口语里常说“调用父类方法”。更准确的说法是:super() 返回一个代理对象,按当前类的 MRO 从下一个位置继续查找。单继承时,效果往往就是“去父类”;多重继承时,它保证协作链不乱、不重复——第五节会看到菱形例子。
class Base:
def __init__(self):
print("Base.__init__")
self.tag = "base"
class Child(Base):
def __init__(self):
print("Child.__init__ start")
super().__init__()
print("Child.__init__ end")
self.extra = 1
c = Child()
print(c.tag, c.extra)
Child.__init__ start
Base.__init__
Child.__init__ end
base 1
3.2 何时调用 super().init
- 子类新增了实例状态,且父类
init负责共享字段 → 通常应调用super().init(...),再赋自己的属性。 - 父类
init有副作用(打开资源、注册表)→ 更要调,否则对象“半初始化”。 - 参数签名变化时,显式传父类需要的参数;不要假设
**kwargs会自动正确(可学协作式传参,入门期先写清楚)。
顺序上,Python 社区常见风格是:先 super().init(...),再初始化子类自己的字段。这样父类方法若在初始化期间被间接触发,至少父类部分已就绪。反过来先写子类字段再 super() 有时也能工作,但与“父类 init 调用可重写方法”叠加时更易踩坑——见第七节。
若父类没有自定义 init,子类通常也不必写无意义的 super().init();但一旦父类后来加上了,子类就要接链。读开源项目时,看到子类 init 第一行 super().init(),多半是在履行这个约定。
3.3 硬编码父类名的代价
class Base:
def __init__(self):
self.base_ok = True
class Child(Base):
def __init__(self):
# 不推荐:写死父类名(多重继承时尤甚)
Base.__init__(self)
self.child_ok = True
c = Child()
print(c.base_ok, c.child_ok)
True True
单继承偶尔能跑,但换成 class Child(Mixin, Base) 后,写死 Base.init 可能跳过 Mixin,破坏协作。习惯用 super(),为 MRO 留好后门。
AI Infra 视角:
nn.Module子类的init里先super().init(),再self.linear = nn.Linear(...)——就是在走协作初始化。漏掉super(),子类可能缺_parameters等内部结构,训练时才爆。
四、多态:同一接口,不同实现
4.1 同一调用点
多态(polymorphism) 在这里取最实用的含义:调用方依赖同一套接口(如 .run(query)),运行时对象可以是不同子类,行为各异。

图3 多态:同一调用点,不同 run 行为
class Tool:
def __init__(self, name: str):
self.name = name
def run(self, query: str) -> str:
raise NotImplementedError
class EchoTool(Tool):
def run(self, query: str) -> str:
return f"echo: {query}"
class LenTool(Tool):
def run(self, query: str) -> str:
return f"len={len(query)}"
def dispatch(tools: dict[str, Tool], name: str, query: str) -> str:
tool = tools[name]
return tool.run(query) # 同一调用点
registry = {
"echo": EchoTool("echo"),
"len": LenTool("len"),
}
print(dispatch(registry, "echo", "hi"))
print(dispatch(registry, "len", "hi"))
echo: hi
len=2
Agent 循环(第 28 篇)正是这种形状:模型选出工具名 → 注册表取出对象 → run → 观察写回对话。
4.2 鸭子类型 vs 名义继承
Python 同时支持两种“像不像”:
- 名义(nominal):靠继承体系与
isinstance——EchoTool标注为Tool的子类。 - 鸭子类型(duck typing):不看家谱,只看有没有所需方法/属性——“走起来像鸭子、叫起来像鸭子,那它就是鸭子”。
class Tool:
pass
class LooseTool:
"""未继承 Tool,但具备 name / run。"""
def __init__(self, name: str):
self.name = name
def run(self, query: str) -> str:
return f"loose:{query}"
def run_any(tool, query: str) -> str:
return tool.run(query) # 只要求有 run
print(run_any(LooseTool("x"), "q"))
print(isinstance(LooseTool("x"), Tool)) # False:名义上不是
loose:q
False
实务建议:框架边界(插件、注册表)可用 ABC / 协议把契约写死(第六节);脚本内部小工具用鸭子类型保持轻。类型注解里的 Protocol(第 18 篇方向)把鸭子想法变成可检查的结构类型——本篇先建立直觉。
4.3 和异常、函数的接点
多态失败时,基类 run 里 raise NotImplementedError 是一种显式信号(第 11 篇:用类型表达失败)。也可用 ABC 在实例化期就禁止未实现子类(见第六节),比拖到第一次调用更早暴露问题。
和函数式编程并不对立:多态的“同一调用点”也可以是一个接受可调用对象的函数——第 08 篇的高阶函数思想。类继承给出有名字的类型层级与共享实现;鸭子类型给出结构化的即插即用。Agent 框架里两者常并存:对外注册要求实现 BaseTool,对内某些适配器只是包了一层 lambda。
再给一个调度上的提醒:多态消除的是调用方的分支,不是错误处理。run 内部仍可能抛 ConfigError / TimeoutError;上层用第 11 篇的刀法按类型捕获。接口一致,不等于永不失败。
五、MRO:方法解析顺序与菱形问题
5.1 属性查找延长成一条链
第 12 篇:查属性先看实例,再看类。有了继承,类之后还要沿基类链继续。这条链的顺序就是 MRO(Method Resolution Order,方法解析顺序)。Python 3 用 C3 线性化 算出一个无重复的类元组。
class Tool:
kind = "tool"
class EchoTool(Tool):
pass
print(EchoTool.__mro__)
print(EchoTool.mro())
print(EchoTool.kind) # 自己没有 → 按 MRO 找到 Tool.kind
(<class '__main__.EchoTool'>, <class '__main__.Tool'>, <class 'object'>)
[<class '__main__.EchoTool'>, <class '__main__.Tool'>, <class 'object'>]
tool
mro 是元组,mro() 返回列表,内容一致。所有新式类最终落到 object。
把第 12 篇的查找规则补全后是:
- 先查实例的命名空间(如
dict); - 再按
type(obj).mro依次查每个类的命名空间; - 描述符、
getattr等特殊钩子会插入额外逻辑(第 14 篇与进阶主题再展开)。
因此“子类重写”本质是:子类在 MRO 里排在父类前面,同名属性先被命中。类属性、方法都走这条链;这也能解释为何实例属性会遮盖类属性——实例这一层更靠前。
5.2 菱形继承
经典菱形:A 继承 B 与 C,而 B、C 都继承 D。若朴素“先左后右再向上”且不做去重,D 可能被访问两次;协作式 super() 也会乱。C3 给出稳定线性序。

super()在B.ping里下一步是C,不是直接跳到D等内容相呼应,直观呈现了菱形继承的MRO线性序。图4 菱形继承与 MRO 线性序
class D:
def ping(self):
return "D"
class B(D):
def ping(self):
return "B:" + super().ping()
class C(D):
def ping(self):
return "C:" + super().ping()
class A(B, C):
def ping(self):
return "A:" + super().ping()
print(A.__mro__)
print(A().ping())
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)
A:B:C:D
直觉(不必背证明):本地优先(先 A)、声明顺序优先(B 在 C 前)、单调性(子类 MRO 尊重基类已有相对序)、每个类只出现一次。super() 在 B.ping 里下一步是 C,不是直接跳到 D——这正是协作式多重继承的精髓。
5.3 何时真的需要读 MRO
- 调试“怎么调到了意料外的实现”;
- 混用多个 Mixin,确认顺序;
- 阅读框架源码里复杂的基类列表。
日常单继承业务代码,心里有“子类 → 父类 → object”就够;一旦出现两个以上基类,打印 mro 比猜更省时间。
若 C3 无法给出合法线性化(例如基类相对顺序互相矛盾),类定义阶段就会直接失败,抛出 TypeError: Cannot create a consistent method resolution order。这是好事:比起运行期随机跳到某个父类,定义期拒绝更安全。入门期你只要知道“不是任意基类列表都能合在一起”。
AI Infra 视角:有些库用多重继承拼“可序列化 + 可配置 + 领域模型”。你要改其中一层的
init或钩子时,先看 MRO,再决定super()链条上谁该先跑。这比死记类图更管用。vLLM / SGLang / 训练框架里若见到一长串基类,先mro再读实现,能少走很多弯路。
六、多重继承、Mixin、ABC 与 AI Infra 场景
6.1 Mixin:横切能力片
Mixin 通常是不单独实例化、只提供一组辅助方法的类,用来和主基类组合:
class LogMixin:
def log(self, msg: str) -> str:
name = getattr(self, "name", "?")
return f"[{name}] {msg}"
class Tool:
def __init__(self, name: str):
self.name = name
def run(self, query: str) -> str:
raise NotImplementedError
class SearchTool(LogMixin, Tool):
def run(self, query: str) -> str:
self.log(f"search {query}") # 来自 Mixin
return f"hits: {query}"
s = SearchTool("search")
print(s.run("mro"))
print(SearchTool.__mro__[0].__name__, [c.__name__ for c in SearchTool.__mro__])
hits: mro
SearchTool ['SearchTool', 'LogMixin', 'Tool', 'object']
基类书写顺序影响 MRO:class SearchTool(LogMixin, Tool) 让 LogMixin 排在 Tool 前。保持 Mixin 又短又正交(少存强制状态、少抢 init),冲突会少很多。
命名上习惯以 Mixin 结尾(或 able 后缀,如 Serializable),提醒读者:“我不是完整领域对象”。文档里写清提供了哪些方法、依赖宿主具备哪些属性(如上例依赖 self.name)。若 Mixin 强依赖宿主字段,却不在文档中声明,使用方会在运行期才发现 AttributeError。
多重继承不是默认选项。优先顺序可以是:单继承 → 组合 → 实在要将横切能力挂到类型上再用 Mixin。这样读代码的人不必先背一张菱形图。
6.2 abc.ABC:接口契约
第 12 篇用 NotImplementedError 提醒子类实现 run;更硬的做法是抽象基类(Abstract Base Class):未实现抽象方法则不能实例化。

图5 Mixin 与 ABC:组合能力 + 接口契约
from abc import ABC, abstractmethod
class BaseTool(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def run(self, query: str) -> str:
"""执行工具。子类必须实现。"""
class EchoTool(BaseTool):
def run(self, query: str) -> str:
return f"echo: {query}"
print(EchoTool("echo").run("ok"))
try:
BaseTool("x") # 未实现抽象方法
except TypeError as e:
print(type(e).__name__ + ":", str(e)[:60] + "...")
echo: ok
TypeError: Can't instantiate abstract class BaseTool without...
(完整报错文字随 Python 小版本略有差异;关注“不能实例化抽象类”。)
这很像“必须实现 forward 的模块接口”:框架在边界上卡住契约,插件作者少漏方法。
6.3 配置子类与 Tool 层次
from abc import ABC, abstractmethod
class ModelConfig:
def __init__(self, name: str, max_tokens: int = 256):
if max_tokens <= 0:
raise ValueError("max_tokens must be positive")
self.name = name
self.max_tokens = max_tokens
class ChatConfig(ModelConfig):
def __init__(self, name: str, max_tokens: int = 256, temperature: float = 0.7):
super().__init__(name, max_tokens)
if not (0.0 <= temperature <= 2.0):
raise ValueError("temperature out of range")
self.temperature = temperature
class BaseTool(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def run(self, query: str) -> str:
...
class EchoTool(BaseTool):
def run(self, query: str) -> str:
return f"echo: {query}"
cfg = ChatConfig("mini-chat", max_tokens=128, temperature=0.2)
print(cfg.name, cfg.max_tokens, cfg.temperature)
print(isinstance(cfg, ModelConfig), isinstance(EchoTool("e"), BaseTool))
mini-chat 128 0.2
True True
AI Infra 视角:采样参数分层(通用生成配置 → 聊天 / 补全特化)、Engine 后端插件、Tokenizer 包装的可替换实现,都受益于「名义类型 + 多态调用」。框架关心 MRO,是因为钩子(
init_subclass、元类、Mixin)常叠在同一条链上——你改一层前先打印mro。
把故事收束:第 12 篇定单个类型的内部结构;本篇定类型之间的关系与查找序;第 14 篇让实例接入 len() / + / with 等语言协议。三条线合在一起,才读得动 nn.Module 与 Agent 框架。
再给一个“继承 + 注册表”的微型骨架,便于你在练习目录里改着玩:
from abc import ABC, abstractmethod
class BaseTool(ABC):
def __init__(self, name: str, description: str = ""):
self.name = name
self.description = description
@abstractmethod
def run(self, query: str) -> str:
...
class EchoTool(BaseTool):
def run(self, query: str) -> str:
return f"echo: {query}"
class UpperTool(BaseTool):
def run(self, query: str) -> str:
return query.upper()
registry: dict[str, BaseTool] = {
t.name: t
for t in (
EchoTool("echo", "echo query"),
UpperTool("upper", "uppercase"),
)
}
for name in ("echo", "upper"):
print(name, "->", registry[name].run("Agent"))
echo -> echo: Agent
upper -> AGENT
注意:推导式里造实例时,抽象方法必须已实现,否则在造实例那一刻就失败——这正是 ABC 相对“懒到调用才 NotImplementedError”的优势。
写推理客户端时,也常见 class BaseClient + class OpenAIClient / class LocalClient:共享重试与日志,子类只覆盖 chat() 的传输细节。密钥与 base_url 放实例属性(第 12 篇提醒),继承只复用流程,不复用密钥本身。
七、踩坑大全
7.1 初始化与重写
坑 1:子类 init 忘了 super().init。 现象:父类字段缺失,AttributeError;或 nn.Module 内部结构未建立。解决:有父类初始化逻辑时显式 super().init(...)。
坑 2:在父类 init 里调用可被子类重写的方法。 现象:子类方法用到“子类 init 后半段才赋值”的字段,于是半初始化态被调用。解决:入门期让 init 只做赋值与校验;复杂复位放到建完后再调(第 12 篇已提示)。
坑 3:重写后仍想用父类逻辑,却复制粘贴一份。 现象:父类改了,子类漏改。解决:super().method(...) 复用。
7.2 类型判断与多态
坑 4:用 type(x) == Base 做分支。 现象:子类实例进不来。解决:isinstance(x, Base)。
坑 5:为“多态”建很深的继承树,实际每层只多一个字段。 现象:改基类牵一片,阅读成本高。解决:先组合 / 参数化;确有共享行为再抽基类。
坑 6:鸭子类型与 isinstance 混用却假设“有 run 就一定是 Tool 子类”。 现象:第三方对象能 run 但注册校验失败。解决:边界上明确要名义类型还是协议;文档写清。
7.3 MRO / 多重继承 / ABC
坑 7:多重继承里写死 Base.init(self)。 现象:跳过 Mixin / 另一父类的初始化。解决:统一 super()。
坑 8:Mixin 抢写 init 且不调用 super()。 现象:链条断裂。解决:Mixin 尽量无状态;若有,按协作式写 super().init(**kwargs)(进阶)或避免在 Mixin 里初始化。
坑 9:菱形下不看 mro 就猜 super() 下一步。 现象:调试像在捉鬼。解决:print(Cls.mro)。
坑 10:ABC 子类漏实现抽象方法,直到测试才发现。 现象:TypeError: Can't instantiate abstract class ...。解决:实现全部 @abstractmethod;可用编辑器 / 类型检查辅助。
7.4 平台与工程
坑 11(Agent):工具注册表用函数字典,后期加权限/超时却无处挂。 现象:横切逻辑复制 N 次。解决:最小基类或 Mixin(日志、计时);注册对象而非裸函数。
坑 12(GPU / 训练):自定义 nn.Module 子类漏 super().init()。 现象:参数注册失败或奇怪报错。解决:子类 init 第一件事(或按框架约定)调用 super().init()。
class Base:
def __init__(self):
self.ready = True
class Bad(Base):
def __init__(self):
self.extra = 1 # 忘了 super
b = Bad()
print(hasattr(b, "ready"), getattr(b, "extra", None))
False 1
八、小结
合上文章后,试着用自己的话解释:“super() 下一步是谁”以及“为什么 type(x) == Base 会伤子类”——讲得清,才算真的会了。动手练习不必一次做完八题,但建议至少跑通:单继承重写、super().init、打印一份 mro、用 ABC 挡一次未实现子类。
- 继承:子类共享父类接口与实现,按需重写或扩展;适合“是一种”的特化,不是唯一的复用手段。
- 类型判断:
isinstance/issubclass认可子类;避免type(x) ==堵死扩展。 super():按 MRO 协作前进;子类初始化有共享字段时务必接好init链。- 多态:同一调用点、不同实现;名义继承与鸭子类型各有场景。
- MRO:C3 线性化给出无重复查找序;菱形下打印
mro,理解super()链条。 - Mixin / ABC:Mixin 组合横切能力;
ABC+@abstractmethod卡住接口契约——Tool / 配置 /nn.Module式 API 的常用刀法。
把故事连起来:对象与引用(02)→ 函数(07/08)→ 异常类型(11)→ 类与实例(12)→ 本篇类型关系、多态与 MRO → 下一篇(14)魔术方法接入数据模型。再往后,dataclass(18)、最小 Agent(28)、nn.Module(40)都会反复用到今天的查找序与协作初始化。
若只能带走一句话:特化靠继承,调度靠多态,查找靠 MRO;不确定时打印 mro。
也可以用“三个不要”自检:不要忘 super().init;不要用 type(...) == 拒绝子类;不要在看不清 MRO 时堆深层多重继承。
再留一个自测:画出 Tool → EchoTool / SearchTool;写出 SearchTool 的 super().init;解释菱形 A(B,C) 的 mro 为何是 A,B,C,D,object;说明 ABC 与 NotImplementedError 谁更早失败——四问都顺,本篇就站稳了。
本篇技能会立刻复利:读开源库时先看子类有没有漏 super();写 Agent 工具时先定基类契约再填实现;遇到古怪的“调到了别的父类方法”,先打印 mro 再猜。先把“类型关系 / 查找序 / 协作初始化”练准,比背三十个设计模式名词更划算。
九、课后练习
- (基础) 定义
class Animal与class Cat(Animal):重写speak;打印isinstance/issubclass结果。 - (基础) 在第 12 篇
Tool上增加LenTool:run返回长度字符串;放进dict[str, Tool]按名dispatch。 - (基础) 写
TimedTool(Tool):init用super()传入name/description,并保存timeout;打印实例属性。 - (理解) 复现坑:子类
init不调super(),访问父类字段触发AttributeError;再修好对比。 - (理解) 构造菱形
D/B/C/A,每类ping用super().ping()拼接标签;打印A.mro与A().ping()。 - (理解) 写一个不继承
Tool但有name/run的类;展示鸭子类型可调用,且isinstance(..., Tool)为False。 - (进阶) 用
LogMixin+BaseTool(ABC)实现SearchTool;确认未实现run的子类无法实例化。 - (进阶 · AI Infra) 实现
ModelConfig与子类ChatConfig(多temperature校验);再写registry: dict[str, BaseTool]跑通两次run。思考:哪些该继承,哪些该组合?
下一篇预告
继承让类型之间有了关系;下一步是让单个实例更好地接入 Python 语言本身的协议——len(obj)、obj + other、for x in obj、with obj、obj == other 背后,是一串以双下划线命名的魔术方法(magic methods / dunder methods)。下一篇《第14篇|魔术方法与数据模型》会系统讲数据模型如何把你的类变成“像内置类型一样好用”的对象,并与第 12、13 篇的属性查找、继承覆盖接轨。
专栏一「Python 从零入门」的后续路线:魔术方法与数据模型(14)→ 专栏二迭代器、装饰器、类型注解与工程化。带着本篇的「MRO / super / 多态」直觉,读训练代码时先看子类有没有漏 super().init()、Tool 注册表依赖的是名义类型还是鸭子约定,再钻模型细节。
若你想提前热身:给 Tool 加 repr 返回 Tool(name=...) 形式(可先查文档或自行试验);观察打印实例时的变化。做完后第 14 篇谈数据模型时你会带着具体场景,而不是抽象术语表。
也可以把第 11 篇的异常树与本篇的 Tool 树对照:except ConfigError 能接住子类异常,和 isinstance(tool, Tool) 能接住子类工具,是同一套“沿继承认可特化”的思想——失败协议与成功路径上的对象形态,都靠继承体系表达变体。
若时间充裕,再多做一件事:对任意子类实例打印 type(obj)、obj.class、type(obj).mro、obj.dict,用笔标注“状态在哪、方法从 MRO 哪一层来”。你会同时复习第 02 篇的对象模型、第 12 篇的属性查找与本篇的线性化——这些都是日后啃 nn.Module 与 Agent 框架的零件。
