大家好,我是正在实战各种 AI 项目的程序员晚枫。
Python 的接口和 Java、C# 不一样——没有 interface 关键字,没有强制实现。 那我们怎么保证代码的"契约"?靠 ABC(抽象基类)和 Protocol(协议类)。今天就把这两种方式讲透彻,让你的代码既灵活又可靠。
🦆 鸭子类型:Python 的默认接口哲学
核心理念
如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Dog: def speak(self): return "Woof!"
class Cat: def speak(self): return "Meow!"
class Robot: def speak(self): return "Beep!"
def make_sound(entity): print(entity.speak())
make_sound(Dog()) make_sound(Cat()) make_sound(Robot())
|
鸭子类型的优点与局限
| 优点 | 局限 |
|---|
| 灵活,不需要继承体系 | 运行时才报错 |
| 代码简洁 | 缺乏文档约束 |
| 天然多态 | IDE 无法智能提示 |
💡 解决方案:用 ABC 做运行时约束,用 Protocol 做静态类型检查。
🎯 抽象基类 ABC:运行时的强制契约
基础用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| from abc import ABC, abstractmethod
class Animal(ABC): """动物抽象基类 —— 子类必须实现所有抽象方法""" @abstractmethod def speak(self): """发出声音""" pass @abstractmethod def move(self): """移动""" pass def describe(self): """非抽象方法:子类可以直接继承""" return f"{self.__class__.__name__}: {self.speak()}, {self.move()}"
class Dog(Animal): def speak(self): return "Woof!" def move(self): return "Running"
dog = Dog() print(dog.describe())
|
抽象属性和类方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| from abc import ABC, abstractmethod
class DataSource(ABC): """数据源抽象基类""" @property @abstractmethod def name(self): """数据源名称""" pass @classmethod @abstractmethod def from_config(cls, config): """从配置创建实例""" pass @abstractmethod def read(self): """读取数据""" pass
class CSVSource(DataSource): def __init__(self, filepath): self._filepath = filepath @property def name(self): return f"CSV:{self._filepath}" @classmethod def from_config(cls, config): return cls(config['filepath']) def read(self): with open(self._filepath) as f: return f.read()
source = CSVSource("/data/test.csv") print(source.name)
|
运行时类型检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from abc import ABC
class Drawable(ABC): def draw(self): pass
class Circle: def draw(self): print("Drawing circle")
circle = Circle() print(isinstance(circle, Drawable))
Drawable.register(Circle) print(isinstance(circle, Drawable)) print(issubclass(Circle, Drawable))
|
何时使用 ABC
- ✅ 框架开发:需要强制用户实现某些方法
- ✅ 运行时类型检查:用
isinstance() 判断 - ✅ 明确的层次结构:确实有 is-a 关系
- ❌ 简单脚本:过度设计
- ❌ 只需要类型提示:用 Protocol 更合适
📋 Protocol 类型提示:静态检查的轻量契约
结构子类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from typing import Protocol
class Drawable(Protocol): """绘制协议 —— 不需要继承,只要结构匹配""" def draw(self) -> None: ...
class Circle: def draw(self) -> None: print("Drawing circle")
class Square: def draw(self) -> None: print("Drawing square")
def render(shape: Drawable): """接受任何实现了 draw 方法的对象""" shape.draw()
render(Circle()) render(Square())
|
带属性的 Protocol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from typing import Protocol, runtime_checkable
@runtime_checkable class Named(Protocol): """有名字的对象""" name: str
class Person: def __init__(self, name: str): self.name = name
class Company: def __init__(self, name: str): self.name = name
def greet(entity: Named): print(f"Hello, {entity.name}!")
greet(Person("Alice")) greet(Company("TechCo"))
print(isinstance(Person("Bob"), Named))
|
Protocol 的组合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from typing import Protocol
class Closeable(Protocol): """可关闭""" def close(self) -> None: ...
class Readable(Protocol): """可读取""" def read(self, size: int = -1) -> str: ...
class Writable(Protocol): """可写入""" def write(self, data: str) -> int: ...
class FileLike(Readable, Writable, Closeable, Protocol): """文件-like 对象""" pass
def process_file(f: FileLike): """接受任何文件-like 对象""" f.write("hello") content = f.read() f.close()
|
⚖️ ABC vs Protocol:如何选择
对比表
| 特性 | ABC | Protocol |
|---|
| 检查时机 | 运行时 | 静态类型检查(mypy) |
| 强制实现 | 是(实例化时检查) | 否(仅类型检查器警告) |
| 需要继承 | 是 | 否(结构匹配) |
| isinstance 支持 | 原生支持 | 需 @runtime_checkable |
| 灵活性 | 低(必须显式继承) | 高(只要结构匹配) |
| 适用场景 | 框架、运行时检查 | 类型提示、接口文档 |
| Python 版本 | 全版本 | 3.8+ |
实战选择指南
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class BasePlugin(ABC): @abstractmethod def execute(self): pass
class Serializable(Protocol): def to_dict(self) -> dict: ...
from typing import Protocol from abc import ABC, abstractmethod
class DataProcessor(Protocol): def process(self, data: bytes) -> str: ...
class BaseProcessor(ABC): @abstractmethod def process(self, data: bytes) -> str: ... def validate(self, data: bytes) -> bool: """默认实现""" return len(data) > 0
|
🚫 避坑指南
陷阱1:Protocol 不能当运行时约束
1 2 3 4 5 6 7 8 9 10 11 12 13
| from typing import Protocol
class Drawable(Protocol): def draw(self) -> None: ...
class Broken: pass
def render(shape: Drawable): shape.draw()
render(Broken())
|
陷阱2:ABC 过度使用
1 2 3 4 5 6 7 8
| class Greeting(ABC): @abstractmethod def say_hello(self): pass
def say_hello(greetable): greetable.say_hello()
|
陷阱3:Protocol 与 dataclass 配合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from typing import Protocol from dataclasses import dataclass
class HasName(Protocol): name: str
@dataclass class Person: name: str age: int
def print_name(obj: HasName): print(obj.name)
print_name(Person("Alice", 30))
|
🎯 本讲总结
鸭子类型:Python 的默认哲学——不检查类型,只看行为。灵活但缺乏保障。
抽象基类 ABC:运行时强制子类实现方法。适合框架开发和运行时类型检查。
Protocol:结构子类型,静态类型检查。不需要继承,只要结构匹配即可。适合类型提示和接口文档。
选择原则:框架用 ABC,类型提示用 Protocol,简单场景用鸭子类型。
📚 推荐教材
《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》
学习路线: 零基础 → 《从入门到实践》 → 《流畅的 Python》 → 本门课程 → 《CPython 设计与实现》
🎓 加入《流畅的 Python》直播共读营
学到这里,如果你想系统吃透这本书——欢迎加入我的直播共读课。
- 每周直播精讲,逐章拆解核心知识点
- 专属学习群,随时答疑交流
- 试运营特惠:
499 元 → 299 元
👉 【立即报名《流畅的 Python》共读课】:https://mp.weixin.qq.com/s/ivHJwn1nNx5ug4TFrapvGg
🔗 课程导航
← 上一讲:继承与组合 | 下一讲:描述符 →
💬 联系我
主营业务:AI 编程培训、企业内训、技术咨询
🎓 AI 编程实战课程
想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!
