今天我们来聊聊 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. 使用 ABC 将 FileHandler 声明为抽象基类
  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. WSGI、mini-web框架

    阅读目录: 1.服务器动态资源请求 2.应用程序示例 3.Web 动态服务器 4.mini-web框架-1-文件结构 5.mini-web框架-2-显示页面 6.mini-web框架-3-替换模板 一 ...

  2. PowerPoint 修改默认导出图片的大小

    1.运行rededit打开注册表编辑器 2.找到HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\PowerPoint\Options 3.其中不同Of ...

  3. MyBatis-Plus条件构造器:构建安全、高效的数据库查询

    一.关于条件构造器(Wrapper) 1.1 简介 MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件.Wrapper 类允许开发者以链式调用的方式构 ...

  4. CSP2023总结

    CSP2023总结 入门组 比赛开始时,顺序开题. 看完 \(T1\) 后没有立即想出正确解法,有点慌乱,看完 \(T2,T3\) 并思考了一会后过去了大概 \(20\) 分钟,想到了解法便看 \(T ...

  5. 使用MySQL Shell 8.4.1-LTS 直接将数据复制到 MySQL实例

    在之前的文章中,我谈到了如何使用 MySQL Shell 通过多线程过程来转储和加载数据,以及如何以不同格式导出表数据,然后可以将这些数据导入到新的 MySQL 实例中.这篇文章将讨论我们如何直接将数 ...

  6. JVM 核心技术

    1.Java代码编译和执行的整个过程: 2. JVM内存管理及垃圾回收机制:  先看图, 二.Java代码编译和执行的整个过程 Java代码编译是由Java源码编译器来完成,流程图如下所示: Java ...

  7. 避免代码冗余,使用接口和泛型重构Java代码

    本文由 ImportNew - ImportNew读者 翻译自 michaelbrameld.如需转载本文,请先参见文章末尾处的转载要求. [感谢 李云涛(@平等的黑)的热心翻译.如果其他朋友也有不错 ...

  8. Clickhouse之常用函数操作

    常用函数汇总 [日期] 注:所有的时间日期函数都可以在第二个可选参数中接受时区参数.示例:Asia / Yekaterinburg.在这种情况下,它们使用指定的时区而不是本地(默认)时区. now() ...

  9. 同步工具之Vector

    官网: https://vector.dev/ 用于构建可观察性管道的轻量级.超快速工具 [安装] curl --proto '=https' --tlsv1.2 -sSf https://sh.ve ...

  10. 【返回值】定义泛型JSON

    /** * 定义统一的Json结构 * 由于封装的Json数据的类型不确定,所以在定义统一的json结构时,我们需要用到泛型. * 统一的json结构中属性包括:数据.状态码.提示信息即可. * 构造 ...