抽象类与接口设计
约 2129 字大约 7 分钟
2026-05-10
在前面的章节中,我们已经学习了类、继承、封装和多态。现在来看一个更偏“设计”的问题:当多个类应该遵守同一套规则时,应该如何表达这套规则?
- 什么是抽象
- 使用 abc 定义抽象类
- 实现抽象类
- 抽象类与多态
- 1写一个 Payment(ABC) + @abstractmethod pay(),子类故意不实现 pay(),实例化时看 TypeError 报“缺少抽象方法”。
- 2在抽象方法里写默认逻辑,子类用 super().method() 调用 —— 抽象不等于只能 pass。
- 3用 @property + @abstractmethod 定义抽象属性,强制子类提供只读属性。
- 4对比一下:能用 ABC 也能用 Protocol —— 前者强制继承,后者只看结构,按需选择。
- 5不要从一开始就堆抽象层:等出现两三个真实重复的实现,再提抽象基类才划算。
在前面的章节中,我们已经学习了类、继承、封装和多态。现在来看一个更偏“设计”的问题:当多个类应该遵守同一套规则时,应该如何表达这套规则?
例如支付系统里有支付宝、微信支付、银行卡支付。它们的内部实现完全不同,但对外都应该提供一个 pay() 方法。如果没有统一约束,调用方只能“相信”每个类都写对了。一旦某个类忘记实现 pay(),错误就会在运行时才暴露出来。
抽象类(Abstract Base Class, ABC)就是为了解决这类问题:它不直接代表某个具体对象,而是定义一套子类必须遵守的接口规范。
什么是抽象
抽象(Abstraction)是忽略细节、提取本质的过程。
在面向对象中:
- 具体类关注“怎么做”
- 抽象类关注“必须能做什么”
比如:
class WeChatPay:
def pay(self, amount):
print(f'使用微信支付 {amount} 元')
class AliPay:
def pay(self, amount):
print(f'使用支付宝支付 {amount} 元')这两个类都能支付,但它们之间没有明确的共同父类。读代码的人只能从方法名上推测:它们应该都符合“支付接口”。
如果我们希望把这种约定写进代码,就可以使用抽象类。
使用 abc 定义抽象类
Python 标准库提供了 abc 模块,用来定义抽象基类。
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass这里有两个关键点:
Payment继承自ABC,表示它是一个抽象基类。pay()被@abstractmethod标记,表示子类必须实现这个方法。
抽象类不能直接实例化:
payment = Payment()
# TypeError: Can't instantiate abstract class Payment without an implementation for abstract method 'pay'这是合理的,因为“支付方式”本身只是一个抽象概念,真正能完成支付的一定是某种具体支付方式。
实现抽象类
子类继承抽象类后,必须实现所有抽象方法,才能被实例化。
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
pass
class WeChatPay(Payment):
def pay(self, amount):
print(f'使用微信支付 {amount} 元')
class AliPay(Payment):
def pay(self, amount):
print(f'使用支付宝支付 {amount} 元')
payments = [WeChatPay(), AliPay()]
for payment in payments:
payment.pay(100)输出:
使用微信支付 100 元
使用支付宝支付 100 元此时调用方可以放心地认为:所有 Payment 子类都一定有 pay() 方法。
抽象类与多态
抽象类常常和多态一起使用。抽象类定义统一接口,具体子类提供不同实现。
class Order:
def __init__(self, amount):
self.amount = amount
def checkout(self, payment: Payment):
payment.pay(self.amount)
order = Order(100)
order.checkout(WeChatPay())
order.checkout(AliPay())checkout() 不关心传进来的是微信支付还是支付宝,只关心它是不是一种 Payment。这就是“一种接口,多种实现”。
抽象方法可以有默认实现
很多人以为抽象方法只能写 pass,其实抽象方法也可以提供默认逻辑。
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, data):
print('开始导出数据')
class CsvExporter(Exporter):
def export(self, data):
super().export(data)
print('导出 CSV 文件')
exporter = CsvExporter()
exporter.export(['Alice', 'Bob'])输出:
开始导出数据
导出 CSV 文件只要方法被 @abstractmethod 标记,子类就必须覆盖它;但子类覆盖后,仍然可以通过 super() 调用父类的默认逻辑。
抽象属性
除了抽象方法,也可以定义抽象属性。
from abc import ABC, abstractmethod
class Storage(ABC):
@property
@abstractmethod
def name(self):
pass
@abstractmethod
def save(self, data):
pass
class LocalStorage(Storage):
@property
def name(self):
return 'local'
def save(self, data):
print(f'保存到本地:{data}')
storage = LocalStorage()
print(storage.name)
storage.save('hello')抽象属性适合表达“子类必须提供某个只读属性”的场景。
ABC 与鸭子类型
Python 一直强调鸭子类型:如果一个对象有我们需要的方法,就可以使用它,不一定要继承某个父类。
class FileLogger:
def write(self, message):
print(f'写入文件:{message}')
class ConsoleLogger:
def write(self, message):
print(f'输出到控制台:{message}')
def log(logger, message):
logger.write(message)上面的 log() 并不要求 logger 继承某个 Logger 类,只要它有 write() 方法即可。这是 Python 很自然的写法。
那么什么时候需要 ABC?
- 需要明确表达一组类属于同一体系时
- 需要强制子类实现某些方法时
- 需要给框架、插件、业务扩展点定义稳定接口时
- 需要让类型提示更清晰时
什么时候不需要 ABC?
- 只是写一个很小的脚本
- 类之间关系很简单
- 只是临时复用一两个方法
- 鸭子类型已经足够清晰
ABC 不是 Java 接口
Python 没有 Java 那种专门的 interface 关键字。ABC 可以承担类似接口的职责,但它仍然是类:
- 可以有普通方法
- 可以有属性
- 可以有默认实现
- 可以和普通继承机制配合
因此,不要机械地把 Java 的接口设计搬到 Python 中。Python 更鼓励在简单场景下直接使用鸭子类型,在复杂场景下再使用 ABC 明确约束。
业务示例:文件导入器
假设系统需要支持 CSV、Excel、JSON 等多种文件导入方式。每种导入器都必须提供 load() 和 validate() 方法。
from abc import ABC, abstractmethod
class Importer(ABC):
@abstractmethod
def load(self, path):
pass
@abstractmethod
def validate(self, rows):
pass
def import_file(self, path):
rows = self.load(path)
self.validate(rows)
print('导入完成')
class CsvImporter(Importer):
def load(self, path):
print(f'读取 CSV:{path}')
return [{'name': 'Alice'}]
def validate(self, rows):
if not rows:
raise ValueError('文件内容不能为空')
class JsonImporter(Importer):
def load(self, path):
print(f'读取 JSON:{path}')
return [{'name': 'Bob'}]
def validate(self, rows):
if not rows:
raise ValueError('文件内容不能为空')
for importer in [CsvImporter(), JsonImporter()]:
importer.import_file('users.data')这里的 Importer 定义了导入器的标准流程:加载、校验、导入。子类只需要实现不同文件格式的细节。
注意事项
- 不要为了“看起来高级”而滥用抽象类。抽象层太多,会让代码变得难读。
- 抽象类适合稳定接口,不适合频繁变化的实验性代码。
- 抽象类关注公共行为,不应该塞进太多具体业务细节。
- 如果只需要表达“有某个方法即可”,可以优先考虑鸭子类型或
Protocol。
总结
抽象类的核心作用是定义规范,而不是复用代码。
- 类描述具体对象
- 抽象类描述一组对象应该具备的能力
ABC和@abstractmethod可以强制子类实现接口- 抽象类常与多态配合使用
- Python 中应在鸭子类型和抽象类之间保持平衡
写 Python OOP 时,不要一开始就设计一堆抽象层。先让代码自然生长,当多个类确实出现稳定的共同接口时,再提取抽象类。
- ABC + @abstractmethod 用来声明“子类必须实现这些方法”,否则实例化时直接 TypeError。
- 抽象方法可以带默认实现,子类覆盖后还能 super().method() 复用 —— 不等于只能 pass。
- Python 没有 interface 关键字,ABC 同时承担接口和基类两种角色。
- 需要强制继承 + 公共默认实现 → 用 ABC;只想表达“有哪些方法”不强制继承 → 用 Protocol。
- 抽象应来自真实重复,不要从需求里幻想接口;过度抽象比没抽象更难维护。
版权所有
版权归属:Shuo Liu
