1. 三个概念为什么总是被混在一起
接口(interface)、抽象基类(ABC)、协议(Protocol)这三个词,是很多 Python 开发者最绕不清的概念。我之前接手一个老项目,代码里用一个 BaseParser 抽象类统一所有解析器,后来新同事直接写了一个不带继承的 Parser,运行时才暴露方法签名不一致,排查花了大半夜。那一刻我意识到,很多人不是不懂语法,而是不理解这三种"接口"方案到底分别在约束什么。
先说结论:Python 里没有 interface 关键字,所以"接口"这件事可以用三种思路来表达。第一种是鸭子类型(Duck Typing),靠的是对象有没有对应的方法;第二种是 abc.ABC 抽象基类,靠的是显式继承和强制实现;第三种是 typing.Protocol 协议,靠的是结构匹配,也就是"长得像就是"。它们解决的问题其实是同一个:怎么在代码里约定"这个对象应该提供哪些能力",避免调用方传进来一个完全不匹配的对象。
这篇文章适合三类人看:刚接触 Python 设计模式的人,想给团队代码加类型约束的人,以及正在设计库/工具 API 的人。你可以把它当成一份接口设计思路的实操手册,不是背语法,而是知道什么场景用哪把刀。
1.1 鸭子类型带来的自由和代价
Python 的灵活很大程度上来自"鸭子类型":只要一个对象能 walk() 能 quack(),它就是鸭子。你不需要继承某个父类,也不需要声明实现了某个接口。
python复制def make_sound(animal):
animal.quack()
只要能调 quack() 的对象传进去都能跑,哪怕是完全不相干的类。这对小型脚本、快速原型来说非常舒服,写起来几乎没有负担。
但代价也很明显:契约是隐形的。调用方只能靠文档、命名规范、代码注释去猜这个对象需要什么方法。一旦某个对象恰好有同名方法但语义不同,或者方法签名对不上,问题要到运行时才暴露。更麻烦的是,IDE 和类型检查器都帮不了你,因为它们不知道 animal 应该长什么样。
1.2 一句话区分三种接口表达
如果非要给一个简单粗暴的记忆方式,我会这么总结:
- 鸭子类型:我没有规定你必须长什么样,但你最好有我要的方法。
- 抽象基类(ABC):你必须认我这个祖宗,并且把我标记的方法都实现出来,否则不让你创建对象。
- 协议(Protocol):你不用继承我,也不用注册,只要结构上具备我声明的方法,类型检查器就认你是我的实现。
这三种不是替代关系,而是互补关系。理解了这个基础,再往下看实际代码就容易了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 抽象基类(ABC):当你想强制子类完成"家庭作业"
抽象基类在 Python 标准库中对应 abc 模块,核心思想是定义一个规定动作的模板,并要求子类把这些动作补齐。比如你定义一个存储接口,要求所有存储后端必须实现 save 和 load,那 ABC 就是最适合的强制工具。
2.1 从零定义一个抽象基类
先看一个最小例子:
python复制from abc import ABC, abstractmethod
from pathlib import Path
class Storage(ABC):
@abstractmethod
def save(self, name: str, data: bytes) -> None: ...
@abstractmethod
def load(self, name: str) -> bytes: ...
这个 Storage 不能直接实例化,因为方法体是空的抽象方法。子类继承了它,只要有一个抽象方法没实现,也无法实例化:
python复制class LocalStorage(Storage):
def save(self, name: str, data: bytes) -> None:
Path(name).write_bytes(data)
def load(self, name: str) -> bytes:
return Path(name).read_bytes()
如果你写一个 IncompleteStorage(Storage),只实现 save 不实现 load,然后试图执行 IncompleteStorage(),Python 会直接抛 TypeError。这相当于运行时给子类做了一次"作业检查",比纯鸭子类型严谨得多。
这里有个很容易忽略的点:必须让类继承 ABC,或者指定 metaclass=ABCMeta,@abstractmethod 才会真正生效。如果你仅仅是:
python复制class Storage:
@abstractmethod
def sa
