K 的一隅

Python Python 语言核心

抽象基类约束什么:ABC 与接口约定

ABC 定义接口骨架;abstractmethod 标记子类必须实现的方法;isinstance 可配合虚拟子类注册做结构检查。

7 分钟阅读 更新于 2026-07-28
本文目录

鸭子类型够用的时候,不必先写接口——「有 read 方法就能当文件用」。但库作者常需要声明:「插件必须提供 save/load 两个方法,否则别继承我的基类。」抽象基类(Abstract Base Class,标准库 abc)在类定义完成、实例化之前就检查子类是否实现了约定方法,缺方法的 concrete 类在 Cls() 时会 TypeError——比运行到一半才 AttributeError 更早失败,也相当于把接口文档写进了类型系统。对插件生态、ORM 基类、Exporter 抽象层这类「第三方会继承」的 API,这一道早期失败能省大量 support 成本。

上下文管理器篇讲资源边界;这一篇讲类型边界:哪些方法是契约、继承关系下谁必须填坑、isinstance 如何配合 ABC 做可扩展的结构判断。Python 没有 Java 的 interface 关键字,ABC 是惯用的近似物;静态类型时代还有 typing.Protocol,二者互补——ABC 偏运行时与继承树,Protocol 偏静态结构检查,同一项目里可以并存。

ABC 与 abstractmethod

abc.ABC 继承(或元类设为 ABCMeta),并把方法标成 @abstractmethod

python
from abc import ABC, abstractmethod


class Repository(ABC):
    @abstractmethod
    def get(self, key: str) -> str | None:
        ...

    @abstractmethod
    def put(self, key: str, value: str) -> None:
        ...


class MemoryRepo(Repository):
    def __init__(self) -> None:
        self._data: dict[str, str] = {}

    def get(self, key: str) -> str | None:
        return self._data.get(key)

    def put(self, key: str, value: str) -> None:
        self._data[key] = value


repo: Repository = MemoryRepo()
# Repository()  # TypeError: Can't instantiate abstract class

未实现的抽象方法可以写 ...(Ellipsis)或 raise NotImplementedError 作占位;子类必须覆盖所有 abstractmethod 才能被实例化。抽象类本身通常不当具体实现用,而是文档 + 静态约束:告诉插件作者「你继承谁就欠哪些方法」。@classmethod@staticmethod@property 也可以与 @abstractmethod 组合;写完后用「故意留空一个方法再实例化」自测一遍最稳。

若子类仍缺抽象成员,错误信息会直接列出方法名,便于大型继承树定位。这比在 __init__ 里手动 if not hasattr(self, "get"): raise ... 更统一,也避免重复样板。

抽象类在继承链里的位置

中间层可以既是抽象类又是别人的子类:实现一部分 abstractmethod,剩下的继续标抽象,直到最底层 concrete 类补全。这适合「模板方法」模式——基类写好流程骨架,子类只填钩子:

python
class BaseExporter(ABC):
    def export(self, rows: list[dict[str, str]]) -> None:
        header = self.format_header()
        body = self.format_rows(rows)
        self.write(header + body)

    @abstractmethod
    def format_header(self) -> str:
        ...

    @abstractmethod
    def format_rows(self, rows: list[dict[str, str]]) -> str:
        ...

    def write(self, text: str) -> None:
        print(text, end="")


class CsvExporter(BaseExporter):
    def format_header(self) -> str:
        return "id,name\n"

    def format_rows(self, rows: list[dict[str, str]]) -> str:
        return "".join(f"{r['id']},{r['name']}\n" for r in rows)

BaseExporter 自己仍不可实例化,但已经提供了可复用的 export 流程。比起在每个子类里复制粘贴相同步骤,抽象基类把「变与不变」切开。标准库 collections.abc 里大量 ABC(SequenceMappingIterable)也是类似思路:定义最小方法集,内置类型注册为虚拟子类。

isinstance 与虚拟子类

ABC 的另一能力是注册:不要求继承,也能让 isinstance 认账:

python
from abc import ABC, abstractmethod


class Serializer(ABC):
    @abstractmethod
    def dumps(self, obj: object) -> bytes:
        ...


class PlainSerializer:
    def dumps(self, obj: object) -> bytes:
        return repr(obj).encode()


Serializer.register(PlainSerializer)

s = PlainSerializer()
assert isinstance(s, Serializer)  # True,尽管没继承 Serializer

register显式 opt-in,不会自动扫描全项目。第三方类在 import 时或插件加载时注册即可参与 isinstance 分支,而不必改框架源码。注意:isinstance(x, MyABC) 检查的是注册或继承关系,不是替你做完整语义验证;方法名对了但行为错了,ABC 拦不住。

issubclass(Child, Parent) 对 ABC 同样适用;虚拟注册只影响 isinstance/isubclass 的「结构 membership」,不改变方法解析顺序(MRO 仍按真实继承来)。

ABC 与鸭子类型怎么选

手段适用
鸭子类型内部小模块、函数接受「有 read 就行」
Protocol静态类型检查、无需继承、结构子类型
ABC强制子类实现、或 provide register + isinstance 扩展

ABC 不能阻止别人写个没继承、却方法齐全的外来类——除非你用 register 或运行时自己校验。它主要约束声明继承你的基类的那条路径。若团队只靠 mypy/pyright,有时 Protocol 更轻;若要在实例化时拦错、或维护插件注册表,ABC 仍很有用。别为了「设计模式」而滥用 ABC:两三个函数的脚本不需要接口层。

collections.abc 与 isinstance 实战

标准库 collections.abc.Mapping 要求 __getitem____iter____len__ 等;dict 在 import 时 register 为虚拟子类,因此:

python
from collections.abc import Mapping

def dump_keys(m: Mapping[str, int]) -> None:
    for k in m:
        print(k)


dump_keys({"a": 1})
assert isinstance({}, Mapping)

写 API 时注解 Mapping 比 concrete dict 宽,调用方可传 dict、只读映射或第三方实现。运行时若要做分支,isinstance(x, Mapping)type(x) is dict 更开放,与鸭子类型精神一致,但比纯鸭子多一层显式契约。

ABCMeta 是 ABC 的元类:class Foo(metaclass=ABCMeta) 与继承 ABC 等价于现代写法。子类化 ABC 时,元类在 class body 执行完后检查 abstract 方法是否齐——这一步发生在 import 定义完成时 对 abstract 类本身,以及 实例化时 对 concrete 子类。中间 abstract 子类缺方法时,实例化该中间类也会失败,而不是拖到更底层。

Protocol 的分工再强调一次:静态工具认 Protocol,运行时插件注册认 ABC.register;二者可同时存在——Protocol 给类型检查器,ABC 给 isinstance 和文档。小项目往往 Protocol 足够;公开扩展点再引 ABC。

抽象方法可以带具体实现体(Python 3.3+):子类仍必须 override,但可以用 super() 调基类里的默认逻辑,类似「可选模板步骤」。这与纯 NotImplementedError 占位不同,适合提供共享校验、日志钩子,而把核心算法留给子类。

注册虚拟子类时,有时配合 __subclasshook__ 做自动结构检查(高级用法):类方法接收 candidate 的 type,返回是否视为子类。collections.abc 部分 ABC 用此减少手动 register。自己写扩展框架时,简单场景 register 足够,不必一上来 subclasshook。

继承 ABC 的类在 MRO 里与普通继承无异;抽象只阻止直接实例化缺方法的类,不改变方法查找顺序。多重继承多个 ABC 时,注意各基类 abstract 方法都要在本类或某个父类里实现,否则实例化仍失败。这与 Java 里 implements 多个 interface 必须全实现的直觉相近,但 Python 允许多继承,冲突由 MRO 解析。

isinstance(obj, ABC) 对未注册、未继承的外来类返回 False,即使方法齐全——若你的框架要「认行为不认血统」,要么 register,要么别用 isinstance,改用 getattr 探测或 Protocol 静态检查。文档里写清扩展方式是继承还是 register,能减少集成方的疑惑。

抽象基类不是「只能有一个子类」:同一 ABC 可有任意多个 concrete 实现,彼此独立。测试里常用「假实现」继承 ABC,只实现 abstract 方法、内存里存 dict,替代真实数据库——因为实例化时会验契约,缺方法立刻失败,比手动 mock 更贴近类型约束。生产代码里则把 ABC 当边界文档:读基类就知道插件作者欠哪些方法,不必翻长篇 README。

下一篇类型标注会讲 Protocolruntime_checkable:那是静态 + 可选 isinstance 的另一条路。本章 ABC 的重点在继承与实例化时的硬性约束;把两条路都看清,选工具时就不会非此即彼。

若你维护的基类将来要加 abstract 方法,所有 concrete 子类在下次 import 后实例化都会失败——这是破坏性变更,应像公共 API 一样版本化、写迁移说明。相较之下,Protocol 新增方法对已有符合结构的类往往无感,这是 ABC 在演进成本上需要权衡的一点。

读 CPython 或大型框架源码时,看到 class Foo(ABC) 可以先扫一遍 @abstractmethod 列表,那就是子类或插件作者的 checklist。isinstance 与 ABC 注册则多出现在「接受多种 backend」的分发逻辑里:序列化、存储驱动、渲染后端,用统一 ABC 类型做参数注解,比 Union 一堆 concrete 类更易扩展。

最后提醒:ABC 解决的是「声明继承时的契约」,不是性能魔法,也不替代单元测试。子类仍可能实现错误语义;抽象只保证「方法存在」,不保证「行为正确」。测试应覆盖 concrete 实现的关键路径,ABC 只是第一道门闩。

小结

ABC + abstractmethod 把接口写成类的一部分,实例化前验证契约。isinstance 配合 register 可在不继承的情况下承认行为兼容的类型。下一篇进入类型标注:把意图写进签名,用工具在运行前抓一批错误。