网站首页 > 编程文章 正文
多年来,我参与过许多 Python 项目,从大型企业系统到模块化库,一个持续的挑战是以清晰、可维护和可扩展的方式定义和实施对象的行为,Python 为此提供了两个强大的工具:协议和抽象基类 (ABC)。
虽然两者都有助于定义对象应该做什么,但它们迎合了不同的场景和思维方式,在这篇文章中,我将向你介绍它们是什么、它们如何工作以及我发现它们何时最有用。
动态 Duck 类型的协议
如果你曾经使用过 Python 的动态鸭子类型方法,你可能已经体验过依赖对象以某种方式“嘎嘎”的自由(和混乱),协议采用这个想法,并通过类型提示将其形式化。
Python 的 typing 模块 (3.8+) 中引入的协议提供了一种无需显式继承即可定义接口的方法,协议定义了对象必须实现的一组方法或属性才能被视为 “兼容”,协议的特别之处在于它们不关心继承 — 任何具有所需方法或属性的对象都满足协议。
数据处理示例
让我们举一个受我使用的财务分析工具启发的示例,系统处理来自 API、数据库和 CSV 文件等各种来源的数据,每个数据源对象都有 and 方法,但它们不共享公共父类,我们需要一种方法来确保这些对象与共享的处理函数一起工作,而无需强制重构。
from typing import Protocol
class DataSource(Protocol):
def read(self) -> str:
...
def write(self, data: str) -> None:
...
class APIClient:
def read(self) -> str:
return "Data from API"
def write(self, data: str) -> None:
print(f"Sending data to API: {data}")
class CSVHandler:
def read(self) -> str:
return "Data from CSV"
def write(self, data: str) -> None:
print(f"Writing to CSV: {data}")
def process_data(source: DataSource) -> None:
data = source.read()
print(f"Processing: {data}")
source.write("Processed data")
api_client = APIClient()
csv_handler = CSVHandler()
process_data(api_client) # Works with APIClient
process_data(csv_handler) # Works with CSVHandler
此方法允许函数接受满足协议的任何对象。这就是为什么我认为协议在这种情况下效果很好:
- 该协议定义了传递给 的任何对象所需的 and 方法.DataSourcereadwriteprocess_data
- 和 都实现了这些方法,但不需要从公共基类继承.APIClientCSVHandler
- 这种灵活性可确保系统可扩展 — 你可以在不修改现有代码的情况下添加新的数据源类型。
根据我的经验,协议在处理遗留代码或集成第三方库时特别有用,由于它们不需要继承,因此它们可以提供类型安全并强制执行行为,而无需强制你重构现有系统。
协议的工作原理
在后台,Python 使用元类使协议同时用作类型提示和运行时验证器,当你使用 Python 定义协议时,Python 会创建一个处理结构类型检查的特殊元类,这意味着,如果对象实现了所需的方法和属性,则该对象被视为协议的虚拟子类。
print(issubclass(APIClient, DataSource)) # True
print(isinstance(csv_handler, DataSource)) # True
既不是 NOR 显式继承自 ,但 Python 的元类机制确保它们符合条件,因为它们实现了 and 方法。
APIClientCSVHandlerDataSourcereadwrite
请注意,如果需要在运行时验证协议,则必须使用模块中的装饰器,没有它,检查将不起作用:@
runtime_checkabletypingisinstanceissubclass
from typing import runtime_checkable
@runtime_checkable
class DataSource(Protocol):
def read(self) -> str:
...
def write(self, data: str) -> None:
...
print(isinstance(api_client, DataSource)) # True
这种灵活性使协议在类型检查方面特别强大,同时保持代码的动态性和可扩展性。
用于设计时结构的抽象基类
协议具有很高的灵活性,但有时你需要更结构化的方法,这就是抽象基类 (ABC) 的用武之地,ABC 是一种工具,通过定义 subclasses 必须实现的严格接口来强制执行一致行为,与协议不同,ABC 需要显式继承,因此当你希望在代码中明确定义层次结构时,它们是更好的选择。
我发现 ABC 在系统的设计阶段特别有用,因为你从头开始构建东西,并希望确保所有子类都遵循一个通用的契约。
报告插件示例
假设我们正在构建一个系统,其中每个插件都会生成一个报告并需要特定的配置,在这里,我们可以使用 ABC 来强制执行一个结构,其中所有插件都实现了 method 和 .generate_reportconfigure
from abc import ABC, abstractmethod
class ReportPlugin(ABC):
@abstractmethod
def generate_report(self, data: dict) -> str:
"""Generate a report based on the given data."""
pass
@abstractmethod
def configure(self, settings: dict) -> None:
"""Configure the plugin with specific settings."""
pass
class PDFReportPlugin(ReportPlugin):
def generate_report(self, data: dict) -> str:
return f"PDF Report for {data['name']}"
def configure(self, settings: dict) -> None:
print(f"Configuring PDF Plugin with: {settings}")
class HTMLReportPlugin(ReportPlugin):
def generate_report(self, data: dict) -> str:
return f"HTML Report for {data['name']}"
def configure(self, settings: dict) -> None:
print(f"Configuring HTML Plugin with: {settings}")
def run_plugin(plugin: ReportPlugin, data: dict, settings: dict) -> None:
plugin.configure(settings)
report = plugin.generate_report(data)
print(report)
pdf_plugin = PDFReportPlugin()
run_plugin(pdf_plugin, {"name": "John Doe"}, {"font": "Arial"})
html_plugin = HTMLReportPlugin()
run_plugin(html_plugin, {"name": "Jane Smith"}, {"color": "blue"})
在此示例中:
- 强制结构:所有插件都必须显式继承并实现 and 方法。ReportPlugingenerate_reportconfigure
- 行为是可预测的:该函数可在任何插件上运行,而无需了解其详细信息。run_plugin
- 可扩展性很简单:添加新插件非常简单,共享接口可确保一致性。
何时使用协议与 ABC
协议和 ABC 之间的选择并不总是非黑即白的,根据我的经验,这通常取决于项目的背景和你的目标,以下是帮助你决定使用哪种方法的一般准则:
在以下情况下使用协议
- 你正在使用现有代码或集成第三方库。
- 灵活性是重中之重,你不希望强制实施严格的层次结构。
- 来自不相关类层次结构的对象需要共享行为。
在以下情况下使用 ABC
- 你正在从头开始设计一个系统,需要强制执行结构。
- 类之间的关系是可预测的,并且继承是有意义的。
- 共享功能或默认行为可以减少重复并提高一致性。
反思
根据我的经验,协议和抽象基类不是相互竞争的工具,它们是互补的,我使用协议将类型安全改造到遗留系统中,而无需进行大量重构,另一方面,在从头开始构建系统时,我一直依赖 ABC,其中结构和一致性至关重要。
在决定使用哪个时,请考虑项目的灵活性需求和长期目标,协议提供灵活性和无缝集成,而 ABC 有助于建立结构和一致性,通过了解它们的优势,你可以选择合适的工具来构建强大、可维护的 Python 系统。
原文
:https://www.tk1s.com/python/protocols-vs-abstract-base-classes-in-python
猜你喜欢
- 2025-06-12 从零开始搭建AI网站(6):如何使用响应式编程
- 2025-06-12 Windows下Ollama安装目录迁移到D盘
- 2025-06-12 RocketMQ的偏移量更新原理(rocketmq迁移)
- 2025-06-12 阿里p8手把手教学,如何写出简洁又规范的单元测试?
- 2025-06-12 Seata源码—7.Seata TCC模式的事务处理
- 2025-06-12 Seata源码—4.全局事务拦截与开启事务处理二
- 2025-06-12 Seata源码—3.全局事务注解扫描器的初始化一
- 2025-06-12 Seata源码—6.Seata AT模式的数据源代理二
- 2025-06-12 Seata源码—6.Seata AT模式的数据源代理三
- 2025-06-12 Seata源码—6.Seata AT模式的数据源代理一
你 发表评论:
欢迎- 06-13边缘计算网关如何实现系统高效运维?
- 06-13虚拟专用网络VPN连接配置
- 06-13WIN10/11下配置VPN(解决L2TP/IPsec无法连接的问题)
- 06-13公共网络上的私有安全通道——VPN
- 06-13远程办公如何访问公司内网办公系统和内部资源?
- 06-13VBRAS场景测试方法—如何高效验证网络设备的性能与稳定性
- 06-13Win10系统如何使用VPN远程办公
- 06-13iPhone轻松实现远程访问公司局域网电脑上的共享文件
- 最近发表
- 标签列表
-
- spire.doc (70)
- instanceclient (62)
- system.data.oracleclient (61)
- 按键小精灵源码提取 (66)
- pyqt5designer教程 (65)
- 联想刷bios工具 (66)
- c#源码 (64)
- graphics.h头文件 (62)
- mysqldump下载 (66)
- sqljdbc4.jar下载 (56)
- libmp3lame (60)
- maven3.3.9 (63)
- 二调符号库 (57)
- git.exe下载 (68)
- diskgenius_winpe (72)
- pythoncrc16 (57)
- solidworks宏文件下载 (59)
- qt帮助文档中文版 (73)
- satacontroller (66)
- hgcad (64)
- bootimg.exe (69)
- android-gif-drawable (62)
- axure9元件库免费下载 (57)
- libmysqlclient.so.18 (58)
- springbootdemo (64)
本文暂时没有评论,来添加一个吧(●'◡'●)