Python 是鸭子类型(duck typing)的天堂——不需要 Java 那种”接口”,你的类只要行为符合期望就行。但项目大了之后,你会想”强制“某些子类必须实现特定方法。这就是抽象基类(Abstract Base Class, ABC) 的用武之地。这一篇讲 abc 模块、ABC、@abstractmethod,以及 Python 3.8+ 引入的 typing.Protocol —— 一种更”鸭子”的类型接口。
一、为什么需要抽象类
看这段代码:
1 2 3 4 5 6 7 8 9 10 11
| class Storage: def save(self, key, value): pass
class FileStorage(Storage): def save(self, key, value): pass
class BadStorage(Storage): pass
|
BadStorage().save(k, v) 什么都不会做(继承的空方法),但代码不报错。这种 bug 隐蔽又痛苦。
抽象类能强制:
1 2 3 4 5 6 7 8 9 10 11
| from abc import ABC, abstractmethod
class Storage(ABC): @abstractmethod def save(self, key, value): ...
class BadStorage(Storage): pass
BadStorage()
|
二、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 36 37 38
| from abc import ABC, abstractmethod
class Shape(ABC): """所有图形的抽象基类。"""
@abstractmethod def area(self) -> float: """计算面积。子类必须实现。"""
@abstractmethod def perimeter(self) -> float: """计算周长。子类必须实现。"""
def describe(self): """具体方法:抽象类里也可以有非抽象方法。""" print(f"面积 {self.area():.2f}, 周长 {self.perimeter():.2f}")
class Circle(Shape): def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
def perimeter(self): return 2 * 3.14159 * self.r
class BrokenShape(Shape): def area(self): return 0
Circle(3).describe() BrokenShape()
|
要点:
- 继承
ABC(等价于 metaclass=ABCMeta);
- 用
@abstractmethod 装饰抽象方法;
- 子类只有实现全部抽象方法后才能实例化;
- 抽象类可以有具体方法(非抽象),子类直接继承。
三、抽象属性与抽象 classmethod / staticmethod
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from abc import ABC, abstractmethod
class Base(ABC): @property @abstractmethod def name(self): ...
@classmethod @abstractmethod def factory(cls): ...
@staticmethod @abstractmethod def helper(): ...
|
注意装饰器顺序:@abstractmethod 一定在最内层(最下面)。
四、Protocol:结构化子类型(Python 3.8+)
Python 3.8 引入了 typing.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): print("画圆")
class Square: def draw(self): print("画方")
def render(item: Drawable): item.draw()
render(Circle()) render(Square())
|
关键:Circle 和 Square 没有继承 Drawable——但因为它们有 draw 方法,就”隐式满足”了这个 Protocol。这就是结构化子类型(structural subtyping)。
Protocol 的优势:
- 不需要修改已有类的继承树;
- 更符合 Python 的鸭子类型精神;
- 与 mypy / pylance 等静态检查工具无缝配合。
runtime_checkable 让 Protocol 支持 isinstance 检查:
1 2 3 4 5 6 7 8
| from typing import Protocol, runtime_checkable
@runtime_checkable class Drawable(Protocol): def draw(self) -> None: ...
isinstance(Circle(), Drawable)
|
五、ABC vs Protocol:什么时候用什么
| 特性 |
ABC |
Protocol |
| 需要显式继承 |
✅ 是 |
❌ 否 |
| 强制实现方法 |
✅ 实例化时报错 |
⚠️ 只在 mypy 时警告 |
| 支持默认实现 |
✅ |
✅ (3.8+) |
isinstance 检查 |
✅ |
✅ 需 @runtime_checkable |
| 侵入性(是否要改子类代码) |
高 |
低 |
规则:
- 想强制运行时检查 → ABC;
- 纯粹类型注解 + 静态检查 → Protocol;
- 面向已有代码/第三方库 → Protocol(不用继承就适配);
- 框架 API(要求实现方一定按契约) → ABC。
六、abc 提供的常用抽象类
Python 标准库里已经定义了很多 ABC,最典型是在 collections.abc:
1 2 3 4 5
| from collections.abc import Iterable, Sized, Container, Mapping, Sequence
isinstance([1, 2, 3], Iterable) isinstance([1, 2, 3], Sized) isinstance({"a": 1}, Mapping)
|
这些基类定义了”什么是可迭代对象”、”什么是序列”等标准接口。写库函数时用它们做参数检查比 isinstance(x, list) 优雅得多——因为你不关心具体类型,只关心行为。
1 2 3 4 5 6 7 8 9 10 11
| def sum_all(items: Iterable[float]) -> float: """接收任何可迭代对象。""" total = 0 for x in items: total += x return total
sum_all([1, 2, 3]) sum_all((1, 2, 3)) sum_all(range(10)) sum_all(x for x in [...])
|
七、注册”虚拟子类”
ABC 支持”注册”一个不实际继承的类作为它的子类:
1 2 3 4 5 6 7 8 9 10 11 12
| from abc import ABC
class Drawable(ABC): pass
class Circle: def draw(self): pass
Drawable.register(Circle)
isinstance(Circle(), Drawable) issubclass(Circle, Drawable)
|
但这不会强制 Circle 实现方法——只是”官方认可”。用于把第三方类”塞进”你的类型系统。日常用得少,知道有这功能即可。
八、抽象方法可以有默认实现
@abstractmethod 装饰的方法可以有方法体,子类可以 super().method() 使用:
1 2 3 4 5 6 7 8 9 10 11 12
| class Base(ABC): @abstractmethod def area(self): return 0
class Circle(Base): def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
|
但子类必须显式重写这个方法名字,才能实例化。这是 abstractmethod 的核心机制。
九、多重继承下的抽象类
多重继承 + 抽象类要小心 MRO。所有抽象方法都必须被 MRO 里某个类实现:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class A(ABC): @abstractmethod def m1(self): ...
class B(ABC): @abstractmethod def m2(self): ...
class C(A, B): def m1(self): print("m1") def m2(self): print("m2")
C()
|
十、typing.Protocol 支持默认实现
Protocol 里的方法可以带默认实现,实现方省略就用默认的:
1 2 3 4 5 6 7 8 9 10 11
| from typing import Protocol
class Loggable(Protocol): name: str def log(self, msg: str) -> None: print(f"[{self.name}] {msg}")
class Service: name = "svc"
Service().log("hi")
|
注意:Protocol 类的方法不会自动继承给实现方。想真的复用,还是 ABC 或 mixin。
十一、一个完整例子:插件系统
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| """基于 ABC 的插件基类,展示抽象接口的实际用途。"""
from abc import ABC, abstractmethod
class Plugin(ABC): """所有插件的基类。"""
@property @abstractmethod def name(self) -> str: ...
@abstractmethod def process(self, data: str) -> str: ...
def on_load(self) -> None: """可选钩子:加载时调用。有默认实现(什么都不做)。"""
def on_unload(self) -> None: """可选钩子:卸载时调用。"""
class UppercasePlugin(Plugin): name = "uppercase"
def process(self, data: str) -> str: return data.upper()
class ReversePlugin(Plugin): name = "reverse"
def process(self, data: str) -> str: return data[::-1]
class BadPlugin(Plugin): name = "bad"
class PluginManager: def __init__(self): self.plugins: list[Plugin] = []
def register(self, plugin: Plugin) -> None: assert isinstance(plugin, Plugin), "必须是 Plugin 的子类" plugin.on_load() self.plugins.append(plugin)
def apply(self, data: str) -> str: for p in self.plugins: data = p.process(data) return data
if __name__ == "__main__": mgr = PluginManager() mgr.register(UppercasePlugin()) mgr.register(ReversePlugin())
print(mgr.apply("hello world"))
try: mgr.register(BadPlugin()) except TypeError as e: print("坏插件被拒绝:", e)
|
十二、常见陷阱
- 忘继承
ABC:@abstractmethod 不生效,子类不实现也能实例化。
- 装饰器顺序错:
@abstractmethod 必须在最内层。
- 忘了实现抽象属性:
property + abstractmethod 组合要写全。
- Protocol 里的方法体被”继承”:其实不会——把默认实现放 Mixin 里。
register 之后子类反而绕过检查:Drawable.register(Circle) 后 Circle 就算不实现 draw 也过 isinstance。用要谨慎。
- 过度设计:小项目直接鸭子类型即可。抽象类是”当你确实需要契约”时的工具,别为了”看起来专业”而滥用。
十三、小结与延伸阅读
- ABC:显式继承 + 强制实现,运行时报错;
- Protocol:结构化匹配,纯类型注解,静态检查;
@abstractmethod 装饰必须实现的方法;
collections.abc 里有 Iterable / Sequence / Mapping 等标准接口;
register 可以把不继承的类”认作”子类;
- Protocol 更”鸭子”,ABC 更严格;
- 简单场景直接用鸭子类型,别过度设计。
延伸阅读:
到这里模块四(面向对象编程)6 篇结束! 下一篇进入 模块五:异常与文件处理,从 异常处理机制 try/except 开始。