今天我们来聊聊 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 代码。它不仅能够捕获接口违规,还能提供更好的代码提示和文档支持。在下一个项目中,不妨试试用抽象基类来设计你的接口!

Python 抽象基类 ABC :从实践到优雅的更多相关文章

  1. Python抽象基类:ABC谢谢你,因为有你,温暖了四季!

    Python抽象基类:ABC谢谢你,因为有你,温暖了四季! Python抽象基类:ABC谢谢你,因为有你,温暖了四季! 实例方法.类方法和静态方法 抽象类 具名元组 参考资料 最近阅读了<Pyt ...

  2. Python抽象基类之声明协议

    抽象基类之--声明协议 上回讲了Python中抽象基类的大概,相信大家对abcmeta以及什么是抽象基类已经有所了解.传送门 现在我们来讲讲抽象基类的另一个常用用法--声明协议 所谓声明协议,有点像J ...

  3. python抽象基类

    抽象基类 抽象基类提了一种方式,用以组织对象的层次结构,做出关于所需方法的断言,以及实现其他一些功能 要定义抽象基类,需要使用abc模块,该模块定义了一个元类(ABCMeta) 和一组装饰器(@abs ...

  4. 抽象基类(ABC),纯虚函数

    #ifndef _ACCTABC_H_ #define _ACCTABC_H_ //(* #include <iostream> #include <string> //*) ...

  5. guxh的python笔记七:抽象基类

    1,鸭子类型和白鹅类型 1.1,白鹅类型 白鹅类型对接口有明确定义,比如不可变序列(Sequence),需要实现__contains__,__iter__,__len__,__getitem__,__ ...

  6. Python高级主题:Python ABC(抽象基类)

    #抽象类实例 作用统一规范接口,降低使用复杂度.import abcclass Animal(metaclass = abc.ABCMeta): ##只能被继承,不能实例化,实例化会报错 @abc.a ...

  7. 【Python】【元编程】【从协议到抽象基类】

    """class Vector2d: typecode = 'd' def __init__(self,x,y): self.__x = float(x) self.__ ...

  8. python面对对象编程---------6:抽象基类

    抽象基本类的几大特点: 1:要定义但是并不完整的实现所有方法 2:基本的意思是作为父类 3:父类需要明确表示出那些方法的特征,这样在写子类时更加简单明白 用抽象基本类的地方: 1:用作父类 2:用作检 ...

  9. 流畅python学习笔记:第十一章:抽象基类

    __getitem__实现可迭代对象.要将一个对象变成一个可迭代的对象,通常都要实现__iter__.但是如果没有__iter__的话,实现了__getitem__也可以实现迭代.我们还是用第一章扑克 ...

  10. Python 接口:从协议到抽象基类

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 15.0px Helvetica } 抽象基类的常见用途:实现接口时作为超类使用.然后,说明抽象基类如何检查 ...

随机推荐

  1. KubeSphere v3.3.1 权限控制详解

    作者:周文浩,青云科技研发工程师,KubeSphere Maintainer.热爱云原生,热爱开源,目前负责 KubeSphere 权限控制的开发与维护. KubeSphere 3.3.1 已经发布一 ...

  2. Ubuntu 22.04 解决和 Windows 共享蓝牙设备的问题

    我有一个 Airpods,连接到 WIndows 可以正常工作,但连接到 ubuntu 后会无法连接,只能删除设备选择重联,但是这又会导致 Windows 不能连接到耳机,只能也删除重新连接,费神费力 ...

  3. 使用BackgroundService创建Windows 服务

    使用管理员权限启动cmd.exe 安装服务 sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.Windo ...

  4. Avalonia开源控件库强力推荐-Semi.Avalonia

    Avalonia是什么? Avalonia是一个强大的框架,使开发人员能够使用.NET创建跨平台应用程序.它使用自己的渲染引擎绘制UI控件,确保在Windows.macOS.Linux.Android ...

  5. 感谢华为:iPhone 16全球价格对比:中国最便宜!比均价低1200元

    相关: https://baijiahao.baidu.com/s?id=1811582397991377070&wfr=spider&for=pc 苹果最新的iPhone 16系列已 ...

  6. 快速量产低功耗 4G 定位方案?Air201 模组来搞定!

    今天我们来了解的是Air201模组快速量产低功耗 4G 定位方案,希望大家有所收获. 寻寻觅觅低功耗4G定位方案? 一个Air201就够了! --定位准.体积小.功耗低,助力行业客户快速量产! 01 ...

  7. Ubuntu 无法播放MP4

    今天用ubuntu打开mp4发现无法播放,然后我以为文件损坏了,就传到手机上面,发现还是可以播放的,然后就查了一下相关资料,发现有人让我安装这个 sudo snap install ffmpeg 安装 ...

  8. 淘宝长仁:JVM性能指标的理论极限和衡量方法(TaobaoJVM)

    在2013年阿里巴巴集团主办的ADC•阿里技术嘉年华,这是一场专属于<互联网工程师>的"技术盛宴",倡导<干货分享>的大会上,51CTO记者有幸采访到了阿里 ...

  9. MongoDB学习笔记之 第1章 MongoDB的安装

    MongoDB学习笔记之 第1章 MongoDB的安装 MongoDB学习笔记之 第2章 MongoDB的增删改查 MongoDB学习笔记之 第3章 MongoDB的Java驱动 MongoDB学习笔 ...

  10. OneForAll - 功能强大的子域收集工具

    OneForAll,是 shmilylty 在 Github 上开源的子域收集工具,目前版本为 v0.4.3. 收集能力强大,利用证书透明度收集子域.常规检查收集子域.利用网上爬虫档案收集子域.利用D ...