请教一个 Python 工程接口设计的问题 - V2EX
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 3169 次点击
    这是一个创建于 1420 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前在开发一个 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     Solana     2649 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 02:19 PVG 10:19 LAX 19:19 JFK 22:19
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86