V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
capbone
V2EX  ›  Python

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

  •  
  •   capbone ·
    qiujueqin · 2021-11-21 17:26:05 +08:00 · 2889 次点击
    这是一个创建于 1127 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前在开发一个 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 对象,而自己分明没有开发过这么一个类。

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

    10 条回复    2021-11-22 12:24:24 +08:00
    hsfzxjy
        1
    hsfzxjy  
       2021-11-21 17:57:25 +08:00   ❤️ 2
    有个概念是 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
        2
    shm7  
       2021-11-21 18:09:07 +08:00
    我想问下,假如读取图像的后缀不同,还没法读了?

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

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

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

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

    除非你要在两个 loader 间 cross reference ,否则不要菱形继承。
    就算对于资源要 cross ref ,有时你也可以分离 resource provider 和 loader
    ekidona
        8
    ekidona  
       2021-11-22 11:29:31 +08:00 via iPhone
    熟读各类框架并 maintain 了一两套内部的发表一下看法:mm 系列的接口设计基本上就是反面教材,mmcv 对 ddp 的额外封装更是 trash, 不要学习它。 相比之下 D2 ,fvcore 那一套 FAIR 实现就漂亮很多
    capbone
        9
    capbone  
    OP
       2021-11-22 11:54:04 +08:00
    @2i2Re2PLMaDnghL 再请教一下,如果资源要 cross reference ,是建议把这些资源直接作为子类 __init__ 的参数吗?比如:
    ```
    class JpegLoader(BaseLoader):
    def load_image(self, arg1, arg2, arg3, ...):
    pass
    ```
    这样?
    2i2Re2PLMaDnghL
        10
    2i2Re2PLMaDnghL  
       2021-11-22 12:24:24 +08:00
    @capbone 视具体的侵入程度,除了你所指的这种,另一种就是分层,用一个类似字典的对象来存储所有的资源(资源供应商、资源管理器、资源注册表、资源池、you name it ),然后把整个资源对象传给 loader ,让 loader 按需摘录资源处理。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2565 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 10:17 · PVG 18:17 · LAX 02:17 · JFK 05:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.