Python · 类、MRO 与对象模型

一切皆对象 — type 与元类、__new__/__init__、C3 线性化、dataclass 与魔术方法协议: 对象行为由协议决定

type(Cls) is type type(obj) is Cls type(type) is type Cls() 实例化 实例 → 类 → 元类 (type) type 所有类的类 = 元类, 默认造类者 Cls (你写的类) 类本身也是对象: 有 __dict__, 可传递 obj (实例) obj.__dict__ 存实例自己的属性 属性查找链: obj.x → 实例 __dict__ → 沿 MRO 查类的 __dict__ 数据描述符 (property) 优先级高于实例字典 菱形继承与 C3 线性化 A B C D(B, C) C3 结果 D.__mro__ : D B C A object C3: 反复读"当前无人继承"的头; B 声明在 C 前 → B 先 A 只出现一次, 排在 B/C 之后 — 公共基类最后汇合 super() = MRO 里的下一个, 不是"父类" B 里的 super().__init__() 在 D 的实例上会走到 C 打印 D.__mro__ / D.mro() 验证顺序 — mixin 链尤其要查 协议表 — 魔术方法决定行为 你写的调用 真正执行的方法 len(obj) __len__ obj[k] / 切片 __getitem__ / __setitem__ a + b __add__ / __radd__ bool(obj) __bool__ 回落 __len__ for x in obj __iter__ + __next__ x in obj __contains__ 回落 __iter__ with obj: __enter__ / __exit__ == / hash __eq__ (配对 __hash__) print / repr(o) __repr__ 实现 __len__ + __getitem__ : len / 切片 / for / in / reversed 全部白得 鸭子类型: 协议满足即兼容, 无需继承谁 Legend 基类 / MRO 节点 实例 / 调用侧 你写的类 元类 / 协议方法 / 重点 super / 要点提示

一切都在查找链上

  • • obj.x: 先实例 __dict__, 再沿 MRO 查类
  • • 赋值 obj.x = v 永远写实例字典
  • • super() 是"MRO 下一个", 不是"父类"
  • • 类属性只放不可变常量, 可变状态进实例

协议即能力

  • • __len__ + __getitem__ = 白得 len/切片/in
  • • __eq__ 与 __hash__ 是一对, 缺一不可哈希
  • • __repr__ 是日志的门面, 值得手写
  • • __enter__/__exit__ 让对象进 with (详见上下文主题)

结构优先于魔法

  • • dataclass 干掉 __init__/__repr__ 样板代码
  • • __slots__ 救海量小对象的内存
  • • ABC 把接口缺失错误提前到启动期
  • • is-a 不成立就用组合, 继承是最强耦合

💡 一句话理解

Python 的对象模型只有一条主规则: 一切皆对象, 行为由协议决定。实例的 type 是类, 类的 type 是 type (元类) — 类自己也是可传递、可修改的对象; 取属性沿"实例 __dict__ → 类的 MRO"查找, 而 len()/下标/加号/in/for 这些语法糖全都翻译成对应的魔术方法 — 你实现协议, 语言就白送你整套能力。

继承体系的核心是 MRO (C3 线性化): super() 的含义是"MRO 里的下一个"而不是"父类", 菱形继承里公共基类只被经过一次。日常工程把机制用在刀刃上: dataclass 定 DTO、__slots__ 省内存、ABC 定契约、Mixin 组合能力 — 能用组合表达的就不动继承, 继承是最强的耦合。

🧠 必知必会 必考 & 必会

__new__ vs __init__
__new__ 造实例 (分配内存, 返回实例, 类方法), __init__ 初始化 (拿到的实例填字段, 不返回); 单例与不可变类型的子类化要在 __new__ 里做文章。
class Single:
    _inst = None
    def __new__(cls):
        if cls._inst is None:             # 关键: __new__ 分配并返回实例
            cls._inst = super().__new__(cls)
        return cls._inst
    # __init__ 只负责填字段, 拿到的正是 __new__ 的返回值
属性查找顺序
obj.x: 数据描述符 (property) → 实例 __dict__ → 沿 MRO 查类字典; 而赋值 obj.x = v 永远写实例字典 (描述符除外)。
class C:
    @property
    def x(self): return self._x     # 数据描述符, 优先级最高
c = C(); c._x = 1
c.__dict__["x"] = 99                 # 实例字典同名也抢不过
print(c.x)                            # → 1  描述符 > 实例 __dict__ > 类
MRO / C3 线性化
方法解析顺序: 子类在前, 保持父类声明顺序, 每个类只出现一次。D(B, C) 的 MRO 是 D, B, C, A, object — 菱形公共基类最后汇合。
class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
print([k.__name__ for k in D.__mro__])
# → ['D', 'B', 'C', 'A', 'object']  B 声明在前, A 只出现一次
super() 的真实含义
不是"父类", 是当前类 MRO 里的下一个; 在哪个类里调用, 就从该类在 MRO 中的位置往后找。协作式多继承全靠它串联。
class B:
    def __init__(self): super().__init__(); print("B")
class C:
    def __init__(self): super().__init__(); print("C")
class D(B, C):
    def __init__(self): super().__init__()    # MRO 里的下一个
D()  # → 先打印 C 再 B: B 里的 super() 走到 C, 不是父类 object
dataclass
装饰器自动生成 __init__/__repr__/__eq__; frozen=True 不可变且可哈希; field(default_factory=list) 防可变默认值共享 — DTO/配置对象的首选。
@dataclass(frozen=True)
class Conf:
    name: str
    tags: list = field(default_factory=list)  # 每实例独立
print(Conf("a"), Conf("a") == Conf("a"))
# → Conf(name='a', tags=[]) True  init/repr/eq 全自动
__slots__
声明固定属性槽, 实例不再带 __dict__: 海量小对象省 ~50% 内存, 属性访问略快; 代价是不能动态加属性 (这是特性也是限制)。
class P:
    __slots__ = ("x", "y")          # 定长槽, 实例无 __dict__
p = P(); p.x = 1
p.z = 2  # → AttributeError: 'P' object has no attribute 'z'
__eq__ 与 __hash__ 契约
定义 __eq__ 而不定义 __hash__, 类自动变 unhashable, 实例进不了 set/当不了 dict key; 相等对象的哈希必须一致。
class T:
    def __init__(self, v): self.v = v
    def __eq__(self, o): return self.v == o.v
{T(1)}  # → TypeError: unhashable type: 'T'
# 对: def __hash__(self): return hash(self.v)  成对实现
property 与描述符
property 是数据描述符的语法糖: 读走 getter、写走 setter, 顺带做校验与惰性计算; 描述符是属性访问协议的底层 (详见描述符主题页)。
class Temp:
    @property
    def c(self): return self._c
    @c.setter
    def c(self, v):
        if v < -273: raise ValueError("below absolute zero")
        self._c = v         # 写走 setter, 顺带校验
ABC 抽象基类
@abstractmethod 声明接口, 子类没实现就在实例化时抛 TypeError — 把"缺方法"的报错从运行期提到启动期。
class Storage(ABC):
    @abstractmethod
    def get(self, key): ...
class Redis(Storage): ...            # 漏实现 get
Redis()  # → TypeError: Can't instantiate abstract class Redis
__repr__ 调试价值
面向开发者的无歧义表示; 日志/REPL/容器打印全走它。目标是 "Task(id='t-91', try=3)" 而不是一串内存地址。
@dataclass
class Task:
    id: str; attempt: int = 0
t = Task("t-91", 3)
print([t])  # → [Task(id='t-91', attempt=3)]  容器打印走 repr
# 手写可输出 Task(id='t-91', try=3) — 排障字段自己挑
Mixin 准则
窄功能、无状态 (或只依赖宿主既有属性)、不单独实例化; 命名以能力收尾 (JSONMixin)。它提供能力组合, 不表达 is-a。
class JSONMixin:                      # 窄能力: 只做导出
    def to_json(self): return json.dumps(self.__dict__)
class Report(JSONMixin):
    def __init__(self, oid): self.oid = oid
Report("r-7").to_json()  # → '{"oid": "r-7"}'  声明能力而非 is-a
元类 metaclass
类的类; type 是默认元类。类创建时走元类的 __new__/__init__, ORM 建表、接口自动注册都靠它 — 但 99% 的业务用 __init_subclass__ 钩子就够了。
class Meta(type):
    def __new__(mcls, name, bases, ns):
        print("造类", name)       # 类创建时执行一次
        return super().__new__(mcls, name, bases, ns)
class S(metaclass=Meta): ...  # → 造类 S  (type 是默认元类)
魔术方法协议
对象行为由 dunder 决定: len→__len__、[]→__getitem__、+→__add__、in→__contains__; 协议满足即获得语言能力, 这是鸭子类型的根基。
class Bag:
    def __len__(self): return 2
    def __getitem__(self, i): return "ab"[i]
b = Bag()
print(len(b), b[0], "a" in b, list(b))  # → 2 a True ['a', 'b']
组合优先
is-a 成立才继承; has-a 用组合/委托 (self.engine.start())。继承把实现细节、变更节奏、MRO 复杂度全部绑死 — 能不继承就不继承。
class Engine:
    def start(self): return "vroom"
class Car:                        # has-a: 组合 + 窄接口
    def __init__(self): self.engine = Engine()
    def start(self): return self.engine.start()
Car().start()  # → vroom

🏭 生产实战 real world

场景 1 · dataclass 定义订单 DTO: frozen 防篡改 + default_factory 防共享

DTO 在服务边界被几十个函数传递, 谁都能改一笔就是隐患。frozen 让写入当场炸, 可变默认值交给工厂:

from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)           # frozen: 下游偷改订单直接炸
class OrderDTO:
    order_id: str
    items: tuple[Item, ...]                     # 不可变集合配 frozen
    tags: frozenset = frozenset()                   # 不可变类型才配当默认值
    meta: dict = field(default_factory=dict)   # 每实例独立 dict

# o.items = (...) → FrozenInstanceError — 配置错误当场暴露, 不带病上线

场景 2 · 千万实例网格: __slots__ 内存减半的量化案例

风控回测要同时持有 1000 万个行情点对象, 默认 __dict__ 版直接把 512MB 容器打爆:

class PointDict:                       # 默认: 每实例背一个 __dict__
    def __init__(self, x, y): self.x, self.y = x, y

class PointSlots:
    __slots__ = ("x", "y")              # 定长槽, 没有 __dict__
    def __init__(self, x, y): self.x, self.y = x, y

# tracemalloc 实测 10_000_000 实例:
#   PointDict  ≈ 1.62 GB  →  PointSlots ≈ 0.76 GB   (-53%)
#   属性访问快 ~10%; 代价: 不能动态加属性 (海量小对象求之不得)

海量小对象 (网格/词表/回测 bar) 一律 slots; 数量级到不了百万就别为它牺牲灵活性。

场景 3 · 单例/连接持有者的三种实现与取舍

全局只要一个的重量级对象, 三种写法各有成本 — 生产默认选最朴素的:

# ① 模块级 (Python 之禅): 模块只执行一次, config = load() 天然单例
#    优点零魔法; 缺点 import 即初始化, 测试要多实例得 hack

# ② __new__ 拦截: 需要"延迟到首次使用"时
class ConnHub:
    _inst = None
    def __new__(cls):
        if cls._inst is None:              # 首次才真正创建
            cls._inst = super().__new__(cls)
        return cls._inst

# ③ metaclass: 所有子类自动单例 — 框架级魔法, 业务别碰
# 生产默认①; 要多环境/易测试 → 工厂 + 显式依赖注入

场景 4 · 自定义容器: 两个魔术方法白得全套序列能力

播放列表类想被 len/切片/for/in 像原生列表一样对待 — 不用继承 list, 实现协议即可:

class Playlist:
    def __init__(self, *tracks): self._t = list(tracks)
    def __len__(self): return len(self._t)
    def __getitem__(self, i):
        if isinstance(i, slice):             # 支持 pl[10:20] 切片
            return Playlist(*self._t[i])
        return self._t[i]
    def __iter__(self): return iter(self._t)

# 白得的能力: len(pl), pl[3], pl[1:5], for t in pl, "x" in pl, list(pl)

场景 5 · Mixin 组合能力 vs 多重继承翻车的重构

三个业务类都要 JSON 导出与审计日志。老代码从公共基类继承, 基类越长越不敢动; 重构成窄 Mixin:

class JSONMixin:                          # 能力: 可序列化; 无状态, 不单用
    def to_json(self):
        return json.dumps(self.__dict__, default=str, ensure_ascii=False)

class AuditMixin:
    def audit(self, actor):                    # 只依赖宿主自带属性
        log.info("audit", obj=type(self).__name__,
                 oid=getattr(self, "id", None), actor=actor)

class Report(JSONMixin, AuditMixin):        # 声明能力而非继承实现
    ...

# Mixin 排 MRO 最左; 动了能力不影响业务类, 各能力可独立测试

场景 6 · ABC 强制子类实现接口: 报错提前到启动期

存储层要换 Redis/MySQL 双实现, 新人漏实现一个方法要到运行到那行才炸。抽象基类把契约钉死:

from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def get(self, key): ...
    @abstractmethod
    def put(self, key, val): ...

class RedisStorage(Storage):
    def get(self, key): return self.r.get(key)
    # 忘了 put → RedisStorage() 当场 TypeError:
    # Can't instantiate abstract class without put — 启动期报错

场景 7 · __enter__/__exit__ 让对象支持 with 计时

给每个 ETL 阶段统一计时打点, 不想在每个函数里复制 try/finally — 做成上下文管理器对象 (机制详见上下文主题):

class timer:                                    # 用法: with timer("etl"): ...
    def __init__(self, name): self.name = name
    def __enter__(self):
        self.t0 = time.perf_counter(); return self
    def __exit__(self, exc_t, exc_v, tb):
        ms = (time.perf_counter() - self.t0) * 1000
        metrics.histogram("phase.ms", ms, phase=self.name)  # 异常也计时
        return False                                # 不吞异常

场景 8 · property 做字段校验与惰性计算

余额字段要"负数进不来 + 首次访问才查账本"。门面用 property, 内部走私有属性, 一道门收口:

class Account:
    def __init__(self, aid): self.aid, self._balance = aid, None

    @property
    def balance(self):                          # 惰性: 首次访问才查账本
        if self._balance is None:
            self._balance = self._fetch_from_ledger()
        return self._balance

    @balance.setter
    def balance(self, v):
        if v < 0: raise ValueError("balance cannot be negative")
        self._balance = v                       # 写内部字段, 不触发自身

场景 9 · __repr__ 让重试日志一眼可读

重试框架打日志全是 "object at 0x7f3a...", 排障时对不上是哪个任务。手写 repr 是性价比最高的投资:

@dataclass
class Task:
    id: str; status: str; attempt: int = 0
    def __repr__(self):                        # 排障字段全带上, 截断防刷屏
        return f"Task(id={self.id!r}, status={self.status}, try={self.attempt})"

# 日志: [retry] Task(id='t-91', status=failed, try=3) — 一眼定位, 可直接 grep
# 默认的 <Task object at 0x7f...> 在排障现场等于什么都没打

场景 10 · __setstate__ 兼容旧版本存档的字段演进

缓存/存档里的对象是旧结构, 新代码加了字段, 反序列化就缺 key。用状态钩子做迁移 (序列化细节见序列化主题):

class Profile:
    def __setstate__(self, state):
        if "created_at" not in state:         # 旧版本存档没有该字段
            state["created_at"] = utcnow()          # 迁移默认值
        if "tags" not in state:
            state["tags"] = []
        self.__dict__.update(state)              # 永远兼容旧快照

# dataclass 等价做法: 新字段给 default; __init__ 缺字段时不会炸

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 类属性放可变对象被全员共享 — class C: items = [], 一个实例 append 全部实例都看得见, 数据莫名串味。正解: 类属性只放不可变常量; 实例级可变状态在 __init__ 里 self.items = []。
# 错: 类属性放可变对象, 全员共享一份
class C: items = []
C().items.append(1); print(C().items)  # → [1]  数据串味
# 对: __init__ 里 self.items = []  每实例独立
坑 2 · 可变默认参数同款翻车 — def f(x, acc=[]): acc 跨调用累积, 第二次调用带着第一次的残留。正解: acc=None 入参, 函数体内 if acc is None: acc = [] — 与坑 1 是同一个根: 默认值只求值一次。
# 错: def f(x, acc=[]) → 默认值只求值一次, 残留跨调用
def f(x, acc=None):
    if acc is None: acc = []      # 对: None 哨兵
    acc.append(x); return acc
f(1); print(f(2))  # → [2]  每次全新
坑 3 · 定义 __eq__ 忘了 __hash__ — 实例放 set/当 dict key 直接 TypeError: unhashable。原因: 定义 __eq__ 会把 __hash__ 置 None。正解: 成对实现 __hash__ = hash((self.a, self.b)) 满足契约, 或 dataclass(frozen=True) 自动生成。
class P:
    def __init__(self, a): self.a = a
    def __eq__(self, o): return self.a == o.a
{P(1)}  # 错: → TypeError: unhashable type: 'P'
# 对: def __hash__(self): return hash(self.a)  成对补上
坑 4 · super() 误当"父类"漏调 — 菱形继承里手写 A.__init__(self, ...) 直接指名道姓, A 被初始化两遍或被跳过。正解: 一律 super().__init__(**kwargs) 沿 MRO 走; 父类签名差异用 kwargs 透传。
# 错: 手写 B.__init__(self); C.__init__(self) 指名道姓,
#     菱形里 A.__init__ 被跑两遍 (或被跳过)
class D(B, C):
    def __init__(self, **kw): super().__init__(**kw)
# 对: 一律 super() 沿 MRO 走, 签名差异用 kwargs 透传
坑 5 · 多重继承同名方法覆盖意外 — D(B, C) 且 B/C 都有 save, 永远是 B 的赢, 你以为调的是 C 的语义。正解: 明确意图 — 显式 D.save = C.save 或拆成 Mixin; 上线前打印 D.__mro__ 确认顺序。
class B:
    def save(self): return "B"
class C:
    def save(self): return "C"
class D(B, C): ...
D().save()  # → B  声明在前的 B 赢, 不是你以为的 C
# 对: 显式 D.save = C.save 表意, 或拆 Mixin; 查 D.__mro__
坑 6 · dataclass 可变默认值 — tags: list = [] 定义时直接 ValueError: mutable default (旧写法静默共享, 更阴险)。正解: field(default_factory=list); frozenset/tuple 这类不可变值可直接当默认。
# 错: @dataclass  class O: tags: list = []
#     → ValueError: mutable default ... for field 'tags'
@dataclass
class Ok:
    tags: list = field(default_factory=list)  # 对
坑 7 · @property 里赋值触发自身死递归 — setter 里写 self.balance = v 又进 setter, RecursionError。正解: 内部一律走私有字段 self._balance, property 只做门面; 新代码可用 __set_name__ 描述符避免手滑。
# 错: @balance.setter 里写 self.balance = v
#     → 又进 setter 自己, RecursionError
@balance.setter
def balance(self, v): self._balance = v  # 对: 写私有字段
坑 8 · __del__ 当资源管理 — 解释器退出、引用环、异常时序都不保证调用 __del__, 文件/连接照样泄漏。正解: with/显式 close + atexit 兜底; __del__ 最多在泄漏时打条告警日志。
# 错: 靠 __del__ 关连接 — 退出/引用环时不保证调, 照样泄漏
with open("a.log") as f: ...  # 对: with 确定性释放
atexit.register(conn.close)      # 对: 进程退出再兜底
坑 9 · 实例属性遮蔽类属性 — 先 self.x = 1 写进实例, 之后改 Cls.x, 这个实例读到的还是旧值, 同一个类两套真相。正解: 类属性当只读常量用 (Cls.x 访问); 会变的状态不放类上。
class C: x = 1
a = C(); a.x = 99        # 写进实例 __dict__
C.x = 100              # 之后改类, a 读到的还是旧值
print(a.x, C().x)         # → 99 100  两套真相
# 对: 会变的状态不放类上, 类属性只当常量
坑 10 · __init__ 里调用可被子类覆写的方法 — 基类 __init__ 里 self.load(), 子类覆写 load 且用到 self.conn (还没赋值) → AttributeError。正解: 模板方法用私有名 self._load(); 或两段式 construct + initialize, 文档写清调用顺序。
class Base:
    def __init__(self):
        self.conn = connect()
        self._load()           # 对: 私有名, 子类覆写不了
# 错: 改成 self.load() — 子类覆写 load 且用到 self.conn,
#     而 conn 还没赋值 → AttributeError
坑 11 · property 滥用把简单字段变慢 — 热路径上每次属性访问都过一次函数调用, 千万次循环里白烧几个点 CPU。正解: 简单字段就是普通属性; property 留给校验/惰性/迁移兼容这三类场景。
class A:
    @property
    def v(self): return self._v   # 错: 简单字段也包一层
class B:
    v = 1                         # 对: 裸属性, 热路径零包装
# property 只留给校验/惰性计算/迁移兼容三类场景
坑 12 · metaclass 冲突 — 两个基类各自的元类不兼容, 类定义行直接 TypeError: metaclass conflict。正解: 写一个共同元类 (继承两者) 或改用 __init_subclass__ 钩子 — 现代优先级永远给后者。
class M1(type): ...
class M2(type): ...
class X(metaclass=M1): ...
class Y(metaclass=M2): ...
class Z(X, Y): ...  # 错: → TypeError: metaclass conflict
# 对: class M12(M1, M2): ...  Z(X, Y, metaclass=M12)
坑 13 · __slots__ 的兼容坑 — 动态加属性 AttributeError、默认没有 __weakref__ (weakref 失效)、老式 pickle/动态 Mixin 翻车。正解: 需要 weakref 显式把 "__weakref__" 加进 slots; 与库冲突就退回普通类, 用工厂函数控制字段。
class S:
    __slots__ = ("a", "__weakref__")  # 对: 要 weakref 显式加槽
s = S(); s.b = 1  # 错: → AttributeError (实例没有 __dict__)
坑 14 · @staticmethod 与 @classmethod 用反 — 工厂方法/读类配置该用 classmethod (拿得到 cls, 子类多态正确); 写成 staticmethod 后硬编码类名, 子类调用行为全错。正解: 需要多态构造用 @classmethod (dict.fromkeys 就是范本), 纯函数才 staticmethod。
class Conn:
    @classmethod
    def from_cfg(cls, path): return cls(parse(path))
# 对: Sub.from_cfg() 拿到 Sub; dict.fromkeys 同款范本
# 错: @staticmethod 里硬编码 return Conn(...) — 子类拿到父类
坑 15 · 继承滥用 — 为了复用 20 行代码继承大基类, is-a 根本不成立 (Car is-a Engine?), 子类背着无关行为到处 isinstance。正解: has-a 用组合/委托, 只暴露需要的窄接口 self.engine.start(); 继承留给真正的类型关系。
# 错: class Car(Engine) — 为复用 20 行, is-a 根本不成立
class Car:                          # 对: 组合 + 窄接口委托
    def __init__(self): self.engine = Engine()
    def start(self): self.engine.start()
坑 16 · 类属性做跨实例计数被并发改丢 — cls.counter += 1 是读-改-写三步, 多线程/asyncio 并发下更新丢失。正解: 加锁或 itertools.count + next(); 多进程下类属性根本不共享, 计数请用 Redis/DB。
class Meter:
    n = 0
    def hit(self): Meter.n += 1  # 错: 读-改-写, 并发丢更新
# 对: with lock: Meter.n += 1  或 next(itertools.count())
坑 17 · frozen dataclass 装可变对象 — @dataclass(frozen=True) 冻结的是属性绑定, 字段里的 list/dict 内容照样被改: o.items.append(x) 不报错。正解: 用 tuple/frozenset/MappingProxyType 装真正不可变的内容。
@dataclass(frozen=True)
class O:
    items: list = field(default_factory=list)
o = O(); o.items.append(1)  # 冻结的是绑定, 内容照样被改
# 对: items: tuple = ()  用不可变容器装内容
坑 18 · __getattr__ 与 __getattribute__ 混淆 — 覆写 __getattribute__ 后所有属性访问都过它, 忘了调 super() 直接 RecursionError, 且性能塌方; __getattr__ 只在正常查找失败后触发。正解: 补缺省用 __getattr__; 确要全量拦截时内部必须 super().__getattribute__(name)。
class A:
    def __getattribute__(self, name):
        return super().__getattribute__(name)  # 对: 必须委托
# 错: return self.__dict__[name] → 又进自己, RecursionError
# 补缺省用 __getattr__: 只在正常查找失败后才触发
坑 19 · MRO 让 Mixin 里的 super 调到意外类 — Mixin 单独用时 super().save() 落到 object 直接 AttributeError; 宿主组合顺序不同, 调到的实现也不同。正解: Mixin 文档写清"宿主链必须提供某方法"; 防御性 getattr(super(), "save", None) 判空。
class JSONMixin:
    def save(self): super().save()    # 单独用 → AttributeError
# 对: fn = getattr(super(), "save", None) 判空再调;
#     文档写清宿主链必须提供 save, 组合顺序不同结果不同
坑 20 · type(x) == Cls 判断丢掉子类 — 子类实例被拒之门外; 反过来一串 if isinstance 先写父类分支, 永远走不到子类分支。正解: 类型判断用 isinstance 且子类分支在前; 精确匹配才用 type(x) is Cls。
class Base: ...
class Sub(Base): ...
print(type(Sub()) == Base)       # 错: → False 子类被拒之门外
print(isinstance(Sub(), Base))   # 对: → True
# if isinstance 分支要把子类写在前面, 否则父类先命中