Python 抽象基类 ABC :从实践到优雅

news2024/12/26 7:08:37

今天我们来聊聊 Python 中的抽象基类(Abstract Base Class,简称 ABC)。虽然这个概念在 Python 中已经存在很久了,但在日常开发中,很多人可能用得并不多,或者用得不够优雅。

让我们从一个实际场景开始:假设你正在开发一个文件处理系统,需要支持不同格式的文件读写,比如 JSON、CSV、XML 等。

初始版本:简单但不够严谨

我们先来看看最简单的实现方式:

class FileHandler:
    def read(self, filename):
        pass
    
    def write(self, filename, data):
        pass

class JsonHandler(FileHandler):
    def read(self, filename):
        import json
        with open(filename, 'r') as f:
            return json.load(f)
    
    def write(self, filename, data):
        import json
        with open(filename, 'w') as f:
            json.dump(data, f)

class CsvHandler(FileHandler):
    def read(self, filename):
        import csv
        with open(filename, 'r') as f:
            return list(csv.reader(f))

这个实现看起来没什么问题,但实际上存在几个隐患:

  1. 无法强制子类实现所有必要的方法
  2. 基类方法的签名(参数列表)可能与子类不一致
  3. 没有明确的接口契约

改进版本:使用抽象基类

让我们引入 abc.ABC 来改进这个设计:

from abc import ABC, abstractmethod

class FileHandler(ABC):
    @abstractmethod
    def read(self, filename: str):
        """读取文件内容"""
        pass
    
    @abstractmethod
    def write(self, filename: str, data: any):
        """写入文件内容"""
        pass

class JsonHandler(FileHandler):
    def read(self, filename: str):
        import json
        with open(filename, 'r') as f:
            return json.load(f)
    
    def write(self, filename: str, data: any):
        import json
        with open(filename, 'w') as f:
            json.dump(data, f)

这个版本引入了两个重要的改进:

  1. 使用 ABCFileHandler 声明为抽象基类
  2. 使用 @abstractmethod 装饰器标记抽象方法

现在,如果我们尝试实例化一个没有实现所有抽象方法的子类,Python 会抛出异常:

# 这个类缺少 write 方法的实现
class BrokenHandler(FileHandler):
    def read(self, filename: str):
        return "some data"

# 这行代码会抛出 TypeError
handler = BrokenHandler()  # TypeError: Can't instantiate abstract class BrokenHandler with abstract method write

进一步优化:添加类型提示和接口约束

让我们再进一步,添加类型提示和更严格的接口约束:

from abc import ABC, abstractmethod
from typing import Any, List, Dict, Union

class FileHandler(ABC):
    @abstractmethod
    def read(self, filename: str) -> Union[Dict, List]:
        """读取文件内容并返回解析后的数据结构"""
        pass
    
    @abstractmethod
    def write(self, filename: str, data: Union[Dict, List]) -> None:
        """将数据结构写入文件"""
        pass
    
    @property
    @abstractmethod
    def supported_extensions(self) -> List[str]:
        """返回支持的文件扩展名列表"""
        pass

class JsonHandler(FileHandler):
    def read(self, filename: str) -> Dict:
        import json
        with open(filename, 'r') as f:
            return json.load(f)
    
    def write(self, filename: str, data: Dict) -> None:
        import json
        with open(filename, 'w') as f:
            json.dump(data, f)
    
    @property
    def supported_extensions(self) -> List[str]:
        return ['.json']

# 使用示例
def process_file(handler: FileHandler, filename: str) -> None:
    if any(filename.endswith(ext) for ext in handler.supported_extensions):
        data = handler.read(filename)
        # 处理数据...
        handler.write(f'processed_{filename}', data)
    else:
        raise ValueError(f"Unsupported file extension for {filename}")

这个最终版本的改进包括:

  1. 添加了类型提示,提高代码的可读性和可维护性
  2. 引入了抽象属性(supported_extensions),使接口更完整
  3. 通过 Union 类型提供了更灵活的数据类型支持
  4. 提供了清晰的文档字符串

使用抽象基类的好处

  1. 接口契约:抽象基类提供了明确的接口定义,任何违反契约的实现都会在运行前被发现。

  2. 代码可读性:通过抽象方法清晰地表明了子类需要实现的功能。

  3. 类型安全:结合类型提示,我们可以在开发时就发现潜在的类型错误。

  4. 设计模式支持:抽象基类非常适合实现诸如工厂模式、策略模式等设计模式。

NotImplementedError 还是 ABC?

很多 Python 开发者会使用 NotImplementedError 来标记需要子类实现的方法:

class FileHandler:
    def read(self, filename: str) -> Dict:
        raise NotImplementedError("Subclass must implement read method")
    
    def write(self, filename: str, data: Dict) -> None:
        raise NotImplementedError("Subclass must implement write method")

这种方式看起来也能达到目的,但与 ABC 相比有几个明显的劣势:

  1. 延迟检查:使用 NotImplementedError 只能在运行时发现问题,而 ABC 在实例化时就会检查。
# 使用 NotImplementedError 的情况
class BadHandler(FileHandler):
    pass

handler = BadHandler()  # 这行代码可以执行
handler.read("test.txt")  # 直到这里才会报错

# 使用 ABC 的情况
class BadHandler(FileHandler):  # FileHandler 是 ABC
    pass

handler = BadHandler()  # 直接在这里就会报错
  1. 缺乏语义NotImplementedError 本质上是一个异常,而不是一个接口契约。

  2. IDE 支持:现代 IDE 对 ABC 的支持更好,能提供更准确的代码提示和检查。

不过,NotImplementedError 在某些场景下仍然有其价值:

  1. 当你想在基类中提供部分实现,但某些方法必须由子类覆盖时:
from abc import ABC, abstractmethod

class FileHandler(ABC):
    @abstractmethod
    def read(self, filename: str) -> Dict:
        pass
    
    def process(self, filename: str) -> Dict:
        data = self.read(filename)
        if not self._validate(data):
            raise ValueError("Invalid data format")
        return self._transform(data)
    
    def _validate(self, data: Dict) -> bool:
        raise NotImplementedError("Subclass should implement validation")
    
    def _transform(self, data: Dict) -> Dict:
        # 默认实现
        return data

这里,_validate 使用 NotImplementedError 而不是 @abstractmethod,表明它是一个可选的扩展点,而不是必须实现的接口。

代码检查工具的配合

主流的 Python 代码检查工具(pylint、flake8)都对抽象基类提供了良好的支持。

Pylint

Pylint 可以检测到未实现的抽象方法:

# pylint: disable=missing-module-docstring
from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def foo(self):
        pass

class Derived(Base):  # pylint: error: Abstract method 'foo' not implemented
    pass

你可以在 .pylintrc 中配置相关规则:

[MESSAGES CONTROL]
# 启用抽象类检查
enable=abstract-method

Flake8

Flake8 本身不直接检查抽象方法实现,但可以通过插件增强这个能力:

pip install flake8-abstract-base-class

配置 .flake8

[flake8]
max-complexity = 10
extend-ignore = ABC001

metaclass=ABCMeta vs ABC

在 Python 中,有两种方式定义抽象基类:

# 方式 1:直接继承 ABC
from abc import ABC, abstractmethod

class FileHandler(ABC):
    @abstractmethod
    def read(self):
        pass

# 方式 2:使用 metaclass
from abc import ABCMeta, abstractmethod

class FileHandler(metaclass=ABCMeta):
    @abstractmethod
    def read(self):
        pass

这两种方式在功能上是等价的,因为 ABC 类本身就是用 ABCMeta 作为元类定义的:

class ABC(metaclass=ABCMeta):
    """Helper class that provides a standard way to create an ABC using
    inheritance.
    """
    pass

选择建议:

  1. 推荐使用 ABC

    • 代码更简洁
    • 更符合 Python 的简单直观原则
    • 是 Python 3.4+ 后推荐的方式
  2. 使用 metaclass=ABCMeta 的场景

    • 当你的类已经有其他元类时
    • 需要自定义元类行为时

例如,当你需要组合多个元类的功能时:

class MyMeta(type):
    def __new__(cls, name, bases, namespace):
        # 自定义的元类行为
        return super().__new__(cls, name, bases, namespace)

class CombinedMeta(ABCMeta, MyMeta):
    pass

class MyHandler(metaclass=CombinedMeta):
    @abstractmethod
    def handle(self):
        pass

实践建议

  1. 当你需要确保一组类遵循相同的接口时,使用抽象基类。

  2. 优先使用类型提示,它们能帮助开发者更好地理解代码。

  3. 适当使用抽象属性(@property + @abstractmethod),它们也是接口的重要组成部分。

  4. 在文档字符串中清晰地说明方法的预期行为和返回值。

通过这个实例,我们可以看到抽象基类如何帮助我们写出更加健壮和优雅的 Python 代码。它不仅能够捕获接口违规,还能提供更好的代码提示和文档支持。在下一个项目中,不妨试试用抽象基类来设计你的接口!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2265703.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

知识图谱+大模型:打造全新智慧城市底层架构

在数字化时代,智慧城市的建设正迎来新一轮的变革。本文将探讨如何结合知识图谱和大模型技术,构建智慧城市的全新底层架构,以应对日益增长的数据量和复杂性,提升城市管理的智能化水平。 知识图谱:智慧城市的知识库 知识…

网络安全 | 云计算中的数据加密与访问控制

网络安全 | 云计算中的数据加密与访问控制 一、前言二、云计算概述2.1 云计算的定义与特点2.2 云计算的服务模式2.3 云计算的数据安全挑战 三、数据加密技术在云计算中的应用3.1 对称加密算法3.2 非对称加密算法3.3 混合加密算法 四、云计算中的访问控制模型4.1 基于角色的访问…

计算机毕业设计Python+卷积神经网络租房推荐系统 租房大屏可视化 租房爬虫 hadoop spark 58同城租房爬虫 房源推荐系统

温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…

攻防世界 - Web - Level 1 unseping

关注这个靶场的其它相关笔记:攻防世界(XCTF) —— 靶场笔记合集-CSDN博客 0x01:Write UP 本关是一个 PHP 代码审计关卡,考察的是 PHP 反序列化漏洞以及命令执行的一些绕过手段,下面笔者将带你一步步过关。…

黑马程序员JavaWeb开发教程(前端部分) ---笔记分享

总结 此篇文章记录的内容是不全的,我觉得基础的部分没有记录,我想主要学的是此课程的后端部分,前端部分学校有学习过,我就开倍速一带而过啦,还有就是学校学的是Vue3和此视频讲的Vue2还是有一定区别的。希望能对大家有…

【统计的思想】统计抽样测试(二)

在统计抽样测试里,一旦我们选定了某个测试方案(n|Ac),我们就可以算出任意不合格品率p对应的接收概率L(p)。把各种可能的p值对应的L(p)连成一条曲线,这就是测试方案(n|Ac)的操作特性曲线。比如,方案(80|1)的操作特性曲线长这个样子…

Pytorch | 利用I-FGSSM针对CIFAR10上的ResNet分类器进行对抗攻击

Pytorch | 利用I-FGSSM针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集I-FGSSM介绍I-FGSSM代码实现I-FGSSM算法实现攻击效果 代码汇总ifgssm.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器: Pytorch | 从零构建AlexNet对CIFAR10进行分类 Pytorch…

【多维DP】力扣576. 出界的路径数

给你一个大小为 m x n 的网格和一个球。球的起始坐标为 [startRow, startColumn] 。你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。你 最多 可以移动 maxMove 次球。 给你五个整数 m、n、maxMove、startRow 以及 startColu…

react防止页面崩溃

在 React 中,ErrorBoundary 组件是一种用于捕获并处理其子组件树中发生的 JavaScript 错误的机制。它可以帮助你在应用程序中实现优雅的错误处理,防止整个应用崩溃,并为用户提供友好的错误提示。ErrorBoundary 通过使用 React 的生命周期方法…

Python使用requests_html库爬取掌阅书籍(附完整源码及使用说明)

教程概述 本教程先是幽络源初步教学分析掌阅书籍的网络结构,最后提供完整的爬取源码与使用说明,并展示结果,切记勿将本教程内容肆意非法使用。 原文链接:Python使用requests_html库爬取掌阅书籍(附完整源码及使用说明…

基于earthSDK三维地图组件开发

上效果 功能点 测量分析相机位置切换geojson数据加载地图打点&#xff0c;显示信息点击回传数据二三位切换 这里二三维切换通上篇openlayers分享&#xff0c;技术交流V:bloxed <template><div class"h100 w100"><div style"width:100%; heig…

基于JavaWeb的流动摊位管理系统

一、系统背景与意义 随着城市化进程的加速和市场经济的发展&#xff0c;流动摊位已经成为城市商业活动中不可或缺的一部分。然而&#xff0c;传统的流动摊位管理方式存在诸多弊端&#xff0c;如信息不透明、管理效率低下、租赁不公等。因此&#xff0c;开发一种高效、便捷、智…

自动驾驶3D目标检测综述(六)

停更了好久终于回来了&#xff08;其实是因为博主去备考期末了hh&#xff09; 这一篇接着&#xff08;五&#xff09;的第七章开始讲述第八章的内容。第八章主要介绍的是三维目标检测的高效标签。 目录 第八章 三维目标检测高效标签 一、域适应 &#xff08;一&#xff09;…

100V宽压输入反激隔离电源,适用于N道沟MOSFET或GaN或5V栅极驱动器,无需光耦合

说明: PC4411是一个隔离的反激式控制器在宽输入电压下具有高效率范围为2.7V至100V。它直接测量初级侧反激输出电压波形&#xff0c;不需要光耦合器或第三方用于调节的绕组。设置输出只需要一个电阻器电压。PC4411提供5V栅极驱动驱动外部N沟道MOSFET的电压或GaN。内部补偿和软启…

1.系统学习-线性回归

系统学习-线性回归 前言线性回归介绍误差函数梯度下降梯度下降示例 回归问题常见的评价函数1. MAE, mean absolutely error2. MSE, mean squared error3. R square &#xff08;决定系数或R方&#xff09; 机器学习建模流程模型正则化拓展阅读作业 链接: 2.系统学习-逻辑回归 …

windows使用zip包安装MySQL

windows通过zip包安装MySQL windows通过zip包安装MySQL下载MySQL的zip安装包创建安装目录和数据目录解压zip安装包创建配置目录 etc 和 配置文件 my.ini安装MySQL进入解压后的bin目录执行命令初始化执行命令安装 验证安装查看服务已安装 启动MySQL查看服务运行情况修改密码创建…

【Postgresql】数据库忘记密码时,重置密码 + 局域网下对外开放访问设置

【Postgresql】数据库忘记密码时,重置密码 + 局域网下对外开放访问设置 问题场景数据库忘记密码时,重置密码局域网下对外开放访问设置问题场景 Postgresql可支持复杂查询,支持较多的数据类型,在生产中较为使用。但有时在局域网下,想通过外部连接使用数据库,可能会出现数…

大模型-使用Ollama+Dify在本地搭建一个专属于自己的聊天助手与知识库

大模型-使用OllamaDify在本地搭建一个专属于自己的知识库 1、本地安装Dify2、本地安装Ollama并解决跨越问题3、使用Dify搭建聊天助手4、使用Dify搭建本地知识库 1、本地安装Dify 参考往期博客&#xff1a;https://guoqingru.blog.csdn.net/article/details/144683767 2、本地…

UE5 崩溃问题汇总!!!

Using bundled DotNet SDK version: 6.0.302 ERROR: UnrealBuildTool.dll not found in "..\..\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll" 在你遇到这种极奇崩溃的BUG &#xff0c;难以解决的时候。 尝试了N种方法&#xff0c;都不行的解决方法。…

数字IC前端学习笔记:脉动阵列的设计方法学(四)

相关阅读 数字IC前端https://blog.csdn.net/weixin_45791458/category_12173698.html?spm1001.2014.3001.5482 引言 脉动结构&#xff08;也称为脉动阵列&#xff09;表示一种有节奏地计算并通过系统传输数据的处理单元(PEs)网络。这些处理单元有规律地泵入泵出数据以保持规则…