抽象类与接口(abc 模块)

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 # 忘了实现 save,但也能实例化!

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() # TypeError: Can't instantiate abstract class 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
# 没实现 perimeter


Circle(3).describe() # ok
BrokenShape() # ❌ TypeError: 没实现 perimeter

要点:

  • 继承 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: # 注意:没继承 Drawable!
def draw(self):
print("画圆")


class Square:
def draw(self):
print("画方")


def render(item: Drawable): # 类型注解
item.draw()


render(Circle()) # ok
render(Square()) # ok

关键CircleSquare 没有继承 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) # True

五、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) # True
isinstance([1, 2, 3], Sized) # True
isinstance({"a": 1}, Mapping) # True

这些基类定义了”什么是可迭代对象”、”什么是序列”等标准接口。写库函数时用它们做参数检查比 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)) # range
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) # True
issubclass(Circle, Drawable) # True

但这不会强制 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 # 默认值,子类可以 super().area() 使用


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() # ok

十、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") # ❌ AttributeError!Protocol 的默认实现不会被继承

注意: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
# plugins.py
"""基于 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"
# 没实现 process


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")) # DLROW OLLEH

try:
mgr.register(BadPlugin()) # TypeError(BadPlugin 没实现 process)
except TypeError as e:
print("坏插件被拒绝:", e)

十二、常见陷阱

  1. 忘继承 ABC@abstractmethod 不生效,子类不实现也能实例化。
  2. 装饰器顺序错@abstractmethod 必须在最内层。
  3. 忘了实现抽象属性property + abstractmethod 组合要写全。
  4. Protocol 里的方法体被”继承”:其实不会——把默认实现放 Mixin 里。
  5. register 之后子类反而绕过检查Drawable.register(Circle)Circle 就算不实现 draw 也过 isinstance。用要谨慎。
  6. 过度设计:小项目直接鸭子类型即可。抽象类是”当你确实需要契约”时的工具,别为了”看起来专业”而滥用。

十三、小结与延伸阅读

  • ABC:显式继承 + 强制实现,运行时报错;
  • Protocol:结构化匹配,纯类型注解,静态检查;
  • @abstractmethod 装饰必须实现的方法;
  • collections.abc 里有 Iterable / Sequence / Mapping 等标准接口;
  • register 可以把不继承的类”认作”子类;
  • Protocol 更”鸭子”,ABC 更严格;
  • 简单场景直接用鸭子类型,别过度设计。

延伸阅读:

到这里模块四(面向对象编程)6 篇结束! 下一篇进入 模块五:异常与文件处理,从 异常处理机制 try/except 开始。