Python 的接口实现有哪些?4 种方式讲清 Duck Typing、ABC 与 Protocol

Python 没有 interface 关键字,但可通过鸭子类型、ABC、typing.Protocol 和 NotImplementedError 定义接口。本文结合可运行示例,对比检查机制与选型,并说明 Protocol 从 Python 3.8、内置泛型从 3.9 开始等版本边界。

10 分钟阅读 Python

Python 的接口实现主要有哪几种

Python 没有 Java、C# 那样的 interface 关键字,但“接口”并没有缺席。常见的 Python 接口实现可以分成四种:

  1. 鸭子类型(Duck Typing):不声明接口,只要对象提供所需方法就能使用。
  2. 抽象基类(ABC):通过继承和 @abstractmethod 定义必须实现的方法。
  3. typing.Protocol:用结构化子类型描述接口,主要交给静态类型检查器验证。
  4. 普通基类配合 NotImplementedError:用运行到方法时才报错的方式约定子类行为。
实现方式 是否要求继承 主要检查时机 能否阻止不完整对象实例化 适合场景
鸭子类型 实际调用时 小型项目、动态对象、简单协作
ABC 通常需要 实例化时 框架基类、插件体系、强运行时约束
Protocol 静态检查时 默认不能 类型标注、跨库协作、低耦合接口
NotImplementedError 通常需要 方法调用时 旧代码、轻量模板、渐进式改造

如果只想记住选型结论:运行时必须拦住不完整实现,用 ABC;希望不强制继承又能做类型检查,用 Protocol;代码足够简单时,鸭子类型就够了。

版本兼容性:Protocol 从 Python 3.8 开始

这几种接口写法出现的时间不同,示例能否直接运行还会受到类型标注语法版本的影响:

功能或语法 标准库支持版本 兼容说明
鸭子类型 所有 Python 3 版本 属于语言的动态类型风格,不依赖专用模块
ABCMeta@abstractmethod Python 3.0+ PEP 3119 为 Python 3.0 引入 ABC 框架
abc.ABC 便捷基类 Python 3.4+ 更早版本需要直接使用 ABCMeta
typing.Protocol Python 3.8+ Python 3.7 及更早版本可从 typing_extensions 导入
typing.runtime_checkable Python 3.8+ 旧版本同样可使用 typing_extensions 的回移实现
dict[str, object] 内置泛型语法 Python 3.9+ Python 3.8 应使用 typing.Dict[str, object]

本文示例以 Python 3.8 及以上版本为最低兼容目标,因此使用 typing.Dict,保证介绍 Protocol 时示例能直接运行。如果项目只支持 Python 3.9+,可以把 Dict[str, object] 改成更现代的 dict[str, object]

在必须维护 Python 3.7 或更早版本时,可以安装 typing_extensions,再使用 from typing_extensions import Protocol, runtime_checkable。不过这些 Python 版本已经停止官方维护,新项目应优先升级运行环境,而不是为了兼容旧版本长期保留额外分支。

方式一:鸭子类型,不声明也能形成接口

鸭子类型关注对象“能做什么”,而不是对象“继承自谁”。函数直接调用约定的方法,只要传入对象具备兼容行为,就可以正常工作。

from typing import Dict


class JsonExporter:
    def export(self, data: Dict[str, object]) -> str:
        return f"JSON:{data}"


class TextExporter:
    def export(self, data: Dict[str, object]) -> str:
        return f"TEXT:{data}"


def build_report(exporter: object, data: Dict[str, object]) -> str:
    # 运行时只关心对象是否真的提供 export 方法
    return exporter.export(data)  # type: ignore[attr-defined]


print(build_report(JsonExporter(), {"total": 3}))
print(build_report(TextExporter(), {"total": 3}))

JsonExporterTextExporter 没有共同父类,但它们都提供了 export(),因此形成了一种非正式接口。

鸭子类型的优点是灵活、低耦合,不需要为简单行为额外设计继承层次。缺点也很直接:如果传错对象,通常要等代码执行到对应方法时才发现问题;IDE 和类型检查器也很难仅凭 object 推断可用成员。

适合鸭子类型的情况:

  • 接口很小,通常只有一两个方法;
  • 调用链短,错误容易定位;
  • 对象来自动态代码或测试替身;
  • 项目暂时没有引入静态类型检查。

当接口逐渐变成公共边界,或者多个团队需要长期协作时,可以进一步改成 Protocol

方式二:ABC 抽象基类,提供运行时约束

abc.ABC@abstractmethod 可以定义正式的抽象基类。子类如果没有实现全部抽象方法,就不能被实例化。

from abc import ABC, abstractmethod
from typing import Dict


class Exporter(ABC):
    @abstractmethod
    def export(self, data: Dict[str, object]) -> str:
        """把数据转换成字符串。"""


class JsonExporter(Exporter):
    def export(self, data: Dict[str, object]) -> str:
        return f"JSON:{data}"


class IncompleteExporter(Exporter):
    pass


print(JsonExporter().export({"total": 3}))

try:
    IncompleteExporter()
except TypeError as exc:
    print(type(exc).__name__)  # TypeError

ABC 的关键价值不是“有一个父类”,而是它会在实例化阶段检查抽象方法。错误能在对象进入业务流程前暴露,比调用到一半再出现 AttributeError 更早。

ABC 适合哪些场景

  • 框架要求插件必须实现固定生命周期方法;
  • 子类需要复用基类中的默认实现;
  • 运行时必须拒绝不完整实现;
  • 接口本身就是清晰的继承关系。

ABC 的虚拟子类要谨慎使用

ABC 还支持 register(),可以把一个没有继承 ABC 的类注册成虚拟子类。这样 isinstance()issubclass() 会认可它,但 ABC 不会进入该类的 MRO,ABC 中的方法也不会自动出现在虚拟子类中。

更重要的是,register() 不会像正常继承那样强制虚拟子类实现抽象方法。因此,如果目标是确保插件完整实现接口,直接继承 ABC 通常更清楚。

标准库已经通过 collections.abc 提供了 IterableIteratorMappingSequence 等常用抽象接口。能够复用现有抽象基类时,不必重复设计一套同名概念。

方式三:Protocol,实现静态鸭子类型

typing.Protocol 把鸭子类型变成了类型检查器能够理解的“结构化接口”。类不需要显式继承 Protocol,只要成员名称和类型签名兼容,就会被视为实现了该协议。

from typing import Dict, Protocol


class SupportsExport(Protocol):
    def export(self, data: Dict[str, object]) -> str:
        """返回导出结果。"""


class JsonExporter:
    def export(self, data: Dict[str, object]) -> str:
        return f"JSON:{data}"


def build_report(
    exporter: SupportsExport,
    data: Dict[str, object],
) -> str:
    return exporter.export(data)


print(build_report(JsonExporter(), {"total": 3}))

JsonExporter 没有继承 SupportsExport,但它的方法签名符合协议。mypy、Pyright 等静态类型检查器可以据此检查参数、返回值和缺失成员。

这让 Protocol 特别适合以下场景:

  • 不能修改第三方类的继承关系;
  • 希望业务层依赖能力,而不是依赖具体实现;
  • 多个互不相关的类恰好提供同一组方法;
  • 需要给 IDE 和类型检查器提供准确提示;
  • 想保留鸭子类型的低耦合特性。

Protocol 与 ABC 的本质区别

ABC 通常采用名义子类型:一个类通过继承明确表示“我是这个接口的实现”。Protocol 采用结构化子类型:不看继承声明,只看对象的结构是否符合要求。

因此,ABC 更像运行时框架契约,Protocol 更像静态类型边界。它们不是互相替代的关系:框架内部可以用 ABC 约束插件基类,对外暴露的函数参数则可以用 Protocol 降低耦合。

runtime_checkable 不是完整的运行时校验

Protocol 默认不能直接用于 isinstance()。添加 @runtime_checkable 后,可以做有限的运行时成员检查:

from typing import Protocol, runtime_checkable


@runtime_checkable
class Closable(Protocol):
    def close(self) -> None:
        """关闭资源。"""


class Connection:
    def close(self) -> None:
        print("closed")


print(isinstance(Connection(), Closable))  # True

这个检查主要确认成员是否存在,不会像静态类型检查器那样完整验证方法参数和返回类型。不要把 @runtime_checkable 当成数据校验器或安全边界;需要严格运行时验证时,应显式检查输入并返回清晰错误。

方式四:基类配合 NotImplementedError

在 ABC 普及之前,很多代码会在基类方法中直接抛出 NotImplementedError

from typing import Dict


class Exporter:
    def export(self, data: Dict[str, object]) -> str:
        raise NotImplementedError("子类必须实现 export")


class JsonExporter(Exporter):
    def export(self, data: Dict[str, object]) -> str:
        return f"JSON:{data}"


class IncompleteExporter(Exporter):
    pass


print(JsonExporter().export({"total": 3}))

try:
    IncompleteExporter().export({"total": 3})
except NotImplementedError as exc:
    print(type(exc).__name__)  # NotImplementedError

这种方式简单,但约束较弱。IncompleteExporter() 可以正常创建,只有调用 export() 时才会失败。如果不完整对象已经进入任务队列、缓存或依赖容器,错误位置可能离根因很远。

它更适合:

  • 维护已有的模板方法代码;
  • 逐步把旧继承体系迁移到 ABC;
  • 基类方法在某些子类中允许不实现,并且调用方知道如何处理。

新设计如果要求“所有具体子类必须实现”,优先使用 ABC,而不是只抛 NotImplementedError

四种接口实现该怎么选

可以按下面的顺序判断:

  1. 是否必须在运行时阻止不完整对象创建?
    是:选择 ABC。

  2. 是否希望实现类不继承共同父类,同时接受静态类型检查?
    是:选择 Protocol。

  3. 接口是否很小,项目也不使用静态类型检查?
    是:鸭子类型通常足够。

  4. 是否在维护历史基类或做渐进式迁移?
    是:可以暂时保留 NotImplementedError,再逐步改成 ABC。

一个常见的工程组合是:

  • 业务函数参数使用 Protocol,只依赖必要能力;
  • 框架插件使用 ABC,保证实现完整;
  • 单元测试传入简单假对象,利用鸭子类型减少样板代码;
  • 旧基类中的 NotImplementedError 逐步迁移,不一次性破坏兼容性。

Python 接口设计的常见误区

把接口写得过大

一个接口塞进十几个不相关方法,会迫使实现类依赖自己不需要的能力。更稳妥的做法是按职责拆成多个小 Protocol 或 ABC,例如把“可读取”“可写入”“可关闭”分别建模。

只用 hasattr 判断实现是否正确

hasattr(obj, "export") 只能说明属性存在,不能保证它可调用,也不能验证参数和返回值。静态场景优先使用 Protocol;运行时边界则应做明确验证。

为了接口而制造继承

如果两个类只是恰好有同一个方法,没有共享实现和生命周期,就不一定需要共同父类。Protocol 可以表达能力,同时保留原有继承结构。

把类型标注当成运行时校验

Python 运行时默认不强制执行类型标注。Protocol 的主要价值来自类型检查器和 IDE;对外部请求、配置文件和不可信数据,仍然要单独做运行时校验。

混淆 NotImplemented 与 NotImplementedError

NotImplementedError 是异常,常用于表示子类没有实现方法;NotImplemented 是一个特殊返回值,主要用于二元运算等协议,通知 Python 尝试其他实现。两者名字相似,但不能互换。

常见问题

Python 有真正的接口吗?

有接口概念,但没有单独的 interface 关键字。鸭子类型提供非正式接口,ABC 提供运行时抽象契约,Protocol 提供静态结构化接口。

ABC 和 Protocol 应该二选一吗?

不一定。ABC 解决“是否按继承契约完整实现”的运行时问题,Protocol 解决“对象结构是否满足调用要求”的静态类型问题。大型项目经常同时使用。

Protocol 会拖慢运行速度吗?

普通 Protocol 类型标注主要供静态工具使用,不会自动在每次函数调用时执行完整接口检查。真正需要关注的是类型检查流程和 @runtime_checkable 检查,而不是把所有类型标注理解成运行时包装器。

第三方对象无法继承我的接口怎么办?

优先使用 Protocol。只要第三方对象已有兼容的方法和类型签名,就能满足协议,无需修改它的继承关系。

结论

  • 追求灵活、接口很小:使用鸭子类型。
  • 需要实例化阶段的强约束和默认实现:使用 ABC。
  • 需要低耦合和静态类型检查:使用 Protocol。
  • 维护旧式模板基类:可以暂用 NotImplementedError,新代码优先考虑 ABC。

技术细节可以继续查阅 Python 官方 abc 文档Python 官方 typing 文档Python 术语表中的鸭子类型以及 PEP 544:Protocols

Practice

读完这一节,去靶场里验证一下。

去挑战广场练习