Python 的接口实现有哪些?4 种方式讲清 Duck Typing、ABC 与 Protocol
Python 没有 interface 关键字,但可通过鸭子类型、ABC、typing.Protocol 和 NotImplementedError 定义接口。本文结合可运行示例,对比检查机制与选型,并说明 Protocol 从 Python 3.8、内置泛型从 3.9 开始等版本边界。
Python 的接口实现主要有哪几种
Python 没有 Java、C# 那样的 interface 关键字,但“接口”并没有缺席。常见的 Python 接口实现可以分成四种:
- 鸭子类型(Duck Typing):不声明接口,只要对象提供所需方法就能使用。
- 抽象基类(ABC):通过继承和
@abstractmethod定义必须实现的方法。 typing.Protocol:用结构化子类型描述接口,主要交给静态类型检查器验证。- 普通基类配合
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}))
JsonExporter 和 TextExporter 没有共同父类,但它们都提供了 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 提供了 Iterable、Iterator、Mapping、Sequence 等常用抽象接口。能够复用现有抽象基类时,不必重复设计一套同名概念。
方式三: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。
四种接口实现该怎么选
可以按下面的顺序判断:
-
是否必须在运行时阻止不完整对象创建?
是:选择 ABC。 -
是否希望实现类不继承共同父类,同时接受静态类型检查?
是:选择 Protocol。 -
接口是否很小,项目也不使用静态类型检查?
是:鸭子类型通常足够。 -
是否在维护历史基类或做渐进式迁移?
是:可以暂时保留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