请教一个 Python 工程接口设计的问题

2021-11-21 17:26:05 +08:00
 capbone

目前在开发一个 AI 相关业务的框架。假设用户需要一个 DataLoader 用于实现从硬盘读取数据,其中每一组数据中包含一张图像数据和一份标签数据。

由于无法预知用户所需的图像和标签格式,因此在基类中我将 load_imageload_label 定义为抽象方法:

from abc import ABC

class BaseLoader(ABC):
    def __init__(self, image_paths, label_paths)
        self.image_paths = image_paths
        self.label_paths = label_paths

    @abstractmethod
    def load_image(self, index):
        pass

    @abstractmethod
    def load_label(self, index):
        pass

用户需要在派生类中自己去定义具体的图像读取方式,比如用户自己写了一个 JpegLoader 类用来读取 jpeg 图像,另一个 TiffLoader 类用来读取 tiff 图像:


@IMAGE_LOADERS.register('jpeg')
class JpegLoader(BaseLoader):
    def load_image(self, index):
        # load jpeg image

@IMAGE_LOADERS.register('tiff')
class TiffLoader(BaseLoader):
    def load_image(self, index):
        # load tiff image

类似地,也有不同的派生类用来定义标签数据的读取方式:


@LABEL_LOADERS.register('json')
class JsonLoader(BaseLoader):
    def load_label(self, index):
        # load json label

@LABEL_LOADERS.register('yaml')
class YamlLoader(BaseLoader):
    def load_label(self, index):
        # load yaml label

在运行阶段,用户通过配置文件从 IMAGE_LOADERSLABEL_LOADERS 注册器中分别选取要调用的派生类,并临时通过菱形继承的方式生成一个新的派生类:


def create_data_loader(image_type, label_type):
    image_loader_cls = IMAGE_LOADERS[image_type]
    label_loader_cls = LABEL_LOADERS[label_type]

    class RuntimeLoader(image_loader_cls, label_loader_cls):
        pass

    return RuntimeLoader()

以上是我针对这种需求想到的一个设计方案。我的问题是:

1 、当需要解耦一个类中的不同功能组件时,让用户自己去定义派生类(比如上面例子中的 JpegLoaderJsonLoader),并由菱形继承的方式再去生成一个新的派生类,这是不是一种合理的设计模式?我总感觉怪怪的,因为一般的对外接口都是提供一个抽象基类,让用户继承自这个基类去定义自己的子类,没见过用这种菱形继承的方式;

2 、最终用来实例化的类是 RuntimeLoader,并不是用户自己去定义的派生类中的任何一个,而且整个 create_data_loader 函数对用户也是封闭的,这会不会让用户觉得很迷惑?比如在 debug 的时候会发现真正的 dataloader 是一个 RuntimeLoader 对象,而自己分明没有开发过这么一个类。

第一次开发比较大的一个工程,希望多家多多指点~

2861 次点击
所在节点    Python
10 条回复
hsfzxjy
2021-11-21 17:57:25 +08:00
有个概念是 Composition over Inheritance Principle ,可以看看这篇文章 https://hynek.me/articles/python-subclassing-redux/

我认为一个理想的做法是 ImageLoader 和 LabelLoader 作为单独两支类,不要继承自 BaseLoader 。然后 create_data_loader 里根据 image_type, label_type 构建相应类的实例,RuntimeLoader.__init__ 则接收一个 ImageLoader 和 LabelLoader 的实例,并在相应的方法中调用。
shm7
2021-11-21 18:09:07 +08:00
我想问下,假如读取图像的后缀不同,还没法读了?

读图像 /label 外,不应该再有个 Transform 的办法?不去看看 TorchVision 的实现?

我是搞不明白这么简单的读图像和 label ,为啥还能搞出几层继承?这设计在代码复杂度大大提升的同时,连基本功能都完不成。

我建议 lz 从使用方角度考虑怎么使用方便,再来设计吧。不懂怎么设计,怎么简单怎么来,不会错很多。搞不懂到处加东西,是很恶心的。
shm7
2021-11-21 18:10:09 +08:00
我觉得这里不需要做二次开发,Torch 的 Dataloader/Dataset 已经帮你做好了。
jaredyam
2021-11-21 19:31:21 +08:00
近年来似乎每个公司的算法部门都在想怎么做一个自己的框架,但似乎都没能逃出 PyTorch 和商汤 mmcv 。怎么说,就你讨论的数据加载和预处理相关模块,以上模块都是以一个数据集作为一个实现单元,而你的想法是以文件类型,感觉这种以类型为捆绑的描述方法有点奇怪?
capbone
2021-11-21 21:32:09 +08:00
@shm7 @jaredyam 题干中的场景是我抽象的,实际业务当然不可能那么简单。我们是做 low-level 图像相关的,要适配不同厂商不同型号的 camera 模组,还要向下游适配不同的 CV 任务。整个框架可以理解为是一个 low-level 版的 mmcv ,但是我们的用户未必是工程师或者开发者,因此不能像 mmcv 那样暴露那么多细化的接口,只能留出基本但是必要的 API 。
capbone
2021-11-21 23:11:08 +08:00
@hsfzxjy 拜读了一下,收获不小。
你说的这个方案我也想过,不过类似上面那个例子中提到的情况,`ImageLoader` 需要获取到 `BaseLoader` 中的一些属性(比如 `image_paths`),如果不使用继承而是使用组合,那么就需要在实例化 `ImageLoader` 的时候把 `image_paths` 作为参数之一?这样似乎又使得整个系统的复杂度变高了?
2i2Re2PLMaDnghL
2021-11-22 11:08:11 +08:00
@capbone 第一,显式传递复杂度反而较低,虽然看上去写了更多代码。
Explicit is better than implicit

第二,菱形继承时运用隐式传递会产生一个重大的问题,就是不写 super() 那句的话「不产生报错但行为不正常」
这个是从 C 开始就花了大力气避免的事情。
反之,如果是显式传递,一旦不写立马报错,很容易排查问题。

除非你要在两个 loader 间 cross reference ,否则不要菱形继承。
就算对于资源要 cross ref ,有时你也可以分离 resource provider 和 loader
ekidona
2021-11-22 11:29:31 +08:00
熟读各类框架并 maintain 了一两套内部的发表一下看法:mm 系列的接口设计基本上就是反面教材,mmcv 对 ddp 的额外封装更是 trash, 不要学习它。 相比之下 D2 ,fvcore 那一套 FAIR 实现就漂亮很多
capbone
2021-11-22 11:54:04 +08:00
@2i2Re2PLMaDnghL 再请教一下,如果资源要 cross reference ,是建议把这些资源直接作为子类 __init__ 的参数吗?比如:
```
class JpegLoader(BaseLoader):
def load_image(self, arg1, arg2, arg3, ...):
pass
```
这样?
2i2Re2PLMaDnghL
2021-11-22 12:24:24 +08:00
@capbone 视具体的侵入程度,除了你所指的这种,另一种就是分层,用一个类似字典的对象来存储所有的资源(资源供应商、资源管理器、资源注册表、资源池、you name it ),然后把整个资源对象传给 loader ,让 loader 按需摘录资源处理。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/816960

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX