Python ISP原则:拒绝“臃肿接口”,90%数据开发者都踩过的隐形坑

Python ISP原则:拒绝“臃肿接口”,90%数据开发者都踩过的隐形坑 一、明明能跑的代码为啥一上线就崩身处数据开发、机器学习领域的朋友, 极有可能遭遇此类令人心烦的状况: 费尽心力编写的模型类, 于本地进行测试时全部通过, 然而一旦实施部署便频繁出现报错分明仅仅是打算调用一个预测方法, 却不得不强行加载一堆根本用不上的接口, 代码复杂臃肿得仿若裹上了厚厚的棉袄。你觉得是自身代码编写得欠缺严谨性吗? 实际上并非如此——这并非是你的过错, 而是你遗漏了SOLID五大原则里最为“低调”但却最为实用的一项原则: 接口隔离原则, 也就是ISP。好多开发者跟着潮流用ABC去给接口下定义, 然而却不清楚, 那种“大而全”的接口, 恰恰是致使代码出现崩溃状况的隐藏着的杀手。ISP的核心要点是很容易理解的: 不给类造成必须去实现它根本用不到的方法的情况。但就是这样一项简单的准则, 能够将80%的代码冗余问题以及运行时错误给解决掉。更重要的点在于, 自身所带的工具, 能够轻易实现落地ISP, 无需额外安装任何依赖, 具备免费开源、易上手的特性, 在相关应用案例上星标数量超过10万, 属于数据开发者必定要学习的底层技巧。就在今天, 将ISP一次性讲解透彻, 从踩坑的案例到实际操作的步骤, 看完后就能直接投入使用。二、核心要点弄明白: 到底啥是ISP呢? 实际操作步骤, 一看就晓得, 先得搞清楚: ISP的核心逻辑并非学术版本的。接口隔离原则的本质是按需分配, 而不是把所有方法扎堆放在一个大接口里, 而是将其拆解成一个个既小巧又专注的接口, 让类仅仅去实现自身真正所需要的能力和特性, 以此来满足特定的功能需求, 进而达成系统的设计目标, 适应不同场景下的变化需求。比如说, 拿手机APP来讲, 就如同微信, 你使用它纯粹是为了聊天以及具有支付功能, 并不需要它拥有视频剪辑以及图片修图的能力要是把所有功能都往微信里面塞, 那么不但会占据内存, 而且还会致使操作变得繁琐, 并且极易出现崩溃的情况。接口也是如此这般, “臃肿接口”最终只会让代码运行速度减慢, 还会引发报错现象。就在当中, 达成ISP的最优工具并非ABC抽象基类这种东西, 而是模块内部所含的那个——它并不需要进行强制继承操作, 只要类能够符合接口所规定的方法要求, 便能够被识别出来, 其灵活性远远超过ABC, 同时也是当前社区予以推荐的接口定义形式。踩坑案例1臃肿模型接口90%新手都会犯不少数据项目刚开始的时候, 开发者会去界定一个所谓“万能”的基础模型接口, 将fit、plot等全部方法都放置进去, 表面上看好像是统一规范的, 实际上却布满了隐患。错误代码复制可运行from abc import ABC, abstractmethod import pandas as pd # 臃肿接口所有模型都必须实现6个方法 class BaseModel(ABC): abstractmethod def fit(self, X: pd.DataFrame, y: pd.Series) - None: ... abstractmethod def predict(self, X: pd.DataFrame) - pd.Series: ... abstractmethod def explain(self, X: pd.DataFrame) - pd.DataFrame: ... abstractmethod def plot(self) - None: ... abstractmethod def save(self, path: str) - None: ... abstractmethod def load(self, path: str) - None: ... # 简单基准模型只需要fit和predict却要实现4个无用方法 class MajorityClassifier(BaseModel): def fit(self, X: pd.DataFrame, y: pd.Series) - None: self._majority y.mode()[0] def predict(self, X: pd.DataFrame) - pd.Series: return pd.Series([self._majority] * len(X)) # 4个用不上的方法只能被迫抛异常 def explain(self, X: pd.DataFrame) - pd.DataFrame: raise NotImplementedError(MajorityClassifier has no explanation.) def plot(self) - None: raise NotImplementedError(MajorityClassifier has nothing to plot.) def save(self, path: str) - None: raise NotImplementedError(MajorityClassifier does not support saving.) def load(self, path: str) - None: raise NotImplementedError(MajorityClassifier does not support loading.)问题相当明显, 这个基准模型仅仅是用于进行对比的, 压根用不到解释、绘图以及保存功能, 然而却不得不去实现4个毫无用处的方法, 这不但使得代码存在冗余情况, 还会致使在运行的时候出现报错, 只要有调用者不经意间调用了或者save方法, 程序便会崩溃, 并且这种错误在代码编写之际根本就察觉不到。正确操作用拆分接口实操可直接套用核心思路是, 将臃肿的部分进行拆分, 使其成为一个个专注于单一能力的接口, 每一个类仅仅实现自身需要的接口, 而不会被强迫去“凑数”。正确代码复制可运行from typing import Protocol import pandas as pd # 拆分接口一个接口对应一个能力 class Fittable(Protocol): def fit(self, X: pd.DataFrame, y: pd.Series) - None: ... class Predictable(Protocol): def predict(self, X: pd.DataFrame) - pd.Series: ... class Explainable(Protocol): def explain(self, X: pd.DataFrame) - pd.DataFrame: ... class Plottable(Protocol): def plot(self) - None: ... class Persistable(Protocol): def save(self, path: str) - None: ... def load(self, path: str) - None: ... # 基准模型只实现自己需要的2个方法简洁高效 class MajorityClassifier: def fit(self, X: pd.DataFrame, y: pd.Series) - None: self._majority y.mode()[0] def predict(self, X: pd.DataFrame) - pd.Series: return pd.Series([self._majority] * len(X)) # 复杂模型实现需要的所有能力不冗余 class RandomForestModel: def fit(self, X: pd.DataFrame, y: pd.Series) - None: from sklearn.ensemble import RandomForestClassifier self._model RandomForestClassifier().fit(X, y) def predict(self, X: pd.DataFrame) - pd.Series: return pd.Series(self._model.predict(X)) def explain(self, X: pd.DataFrame) - pd.DataFrame: importances self._model.feature_importances_ return pd.DataFrame({feature: X.columns, importance: importances}) def save(self, path: str) - None: import joblib joblib.dump(self._model, path) def load(self, path: str) - None: import joblib self._model joblib.load(path)调用者也只需声明自己需要的能力避免调用无用方法报错def evaluate_model(model: Predictable, X: pd.DataFrame, y: pd.Series) - float: from sklearn.metrics import accuracy_score return accuracy_score(y, model.predict(X)) # 只能传入有predict方法的模型传入MajorityClassifier正常运行 # 若传入没有predict方法的类编写时就会被mypy报错提前规避问题 evaluate_model(MajorityClassifier(), Xpd.DataFrame(), ypd.Series())踩坑案例2臃肿数据源接口数据工程高频坑在数据工程里, 除了模型之外, 也常常会浮现出相近似的这么些问题: 确定一个统一的数据源接口, 将那连接、读取、流式处理、分页以及缓存等诸般方法全部堆砌进去, 致使CSV、API、内存等不一样的数据源不得不去实现毫无用处的方法。错误代码简化版from abc import ABC, abstractmethod import pandas as pd class DataSource(ABC): abstractmethod def connect(self) - None: ... abstractmethod def read(self) - pd.DataFrame: ... abstractmethod def stream(self) - None: ... abstractmethod def paginate(self, page_size: int) - pd.DataFrame: ... # CSV数据源不需要连接、流式、分页却要被迫实现 class CsvDataSource(DataSource): def connect(self) - None: pass # 空实现无意义 def read(self) - pd.DataFrame: return pd.read_csv(self._path) def stream(self) - None: raise NotImplementedError(CSV不支持流式处理) def paginate(self, page_size: int) - pd.DataFrame: raise NotImplementedError(CSV不支持分页)等同地, 在进行拆分之后, 代码将会变得简洁同时具备安全性, 具体而言, 重构代码这一行为能够参考上面所提及的模型案例, 其核心逻辑是完全一样的, 也就是按照能力去拆分接口, 依据需求来实现。进阶技巧协议组合按需组合能力是这样的, 要是调用者有多个能力方面所需, 那么并不用去重新定义接口, 而是直接把已然存在的小接口进行组合就行, 如此一来灵活性就被拉到最满的程度:from typing import Protocol import pandas as pd # 组合接口同时具备读取和连接能力 class ManagedSource(Readable, Connectable, Protocol): ... # 只接收同时具备这两个能力的数据源避免无效调用 def run_managed_query(source: ManagedSource) - pd.DataFrame: source.connect() data source.read() source.disconnect() return data三、辩证分析ISP不是“万能药”这些情况别硬用ISP的价值是被肯定的, 它能够将代码冗余的问题、运行时报错的问题以及接口不清晰的问题彻底解决, 使得代码在维护方面变得更加容易, 在扩展方面也变得更加容易, 特别适用于多模型、多数据源的大型项目, 还能够让协作成本被大幅降低。然而从辩证的角度去看, ISP是存在其自身局限性的, 要是盲目地进行拆分接口的话, 那反而会出现与预期相反的情况, 进而增加开发成本。对于这3种情形而言, 是不用强行去应用ISP的:1. 实现类个个都得有接口里的全部方法: 要是在你的项目当中, 每个模型都非得有fit、save等所有那些方法, 那么一个统一的接口反倒会更具高效性, 拆分之后只会让接口数量增多, 白白增添麻烦。2. 仅有一个实现类: 要是在你的代码当中, 某一个接口仅有一个实现类, 不存在别的扩展需求, 那就没必要进行拆分——而拆分接口的关键核心在于适配多个实现类的各异需求, 单个实现类根本没这个必要。3. 针对单一方法的那种“无效拆分”就是, 倘若存在一个接口, 该接口仅有一个方法, 并且这个方法仅仅在一个地方被使用了。那么拆分之后, 不但不会带来任何实际价值, 反而会只会增加代码的间接性还会让代码变得更加难以理解。那真正的判断标准究竟是什么, 是: 你眼下是不是处在被迫书写raise或者进行空实现这样一种状况, 如果是这种情况的话, 那就表明接口显得过于臃肿了, 此时需要运用ISP要是并非如此的话, 那可就没有必要多此一举。另外, 还需要留意3个坑, 其一, 协议组合应当规范, 要防止定义出一堆杂乱无章的小接口, 建议按照“能力类型”进行命名, 比如、这般, 并放置在专门的模块当中其二, 必须搭配静态类型检查工具像mypy、之类的, 不然无法发挥其作用, 依旧会出现运行时错误其三, 不要混合运用ABC和, 这两者的适配性欠佳, 容易致使接口变得混乱。四、现实意义学会ISP能解决哪些实际问题数据开发者而言, 机器学习工程师来讲, ISP并非那种华而不实的理论, 而是可直接解决工作里痛点的实用技巧, 其价值主要体现于三个方面:1. 运行时错误得以减少, 调试成本随之降低, 不必再顾虑调用者错误地调用了无用方法进而致使崩溃, 静态类型检查能够提前发觉问题, 无需等到上线之后才去排查报错, 节省了大量的调试时间。2. 使得代码冗余度降低从而提升可维护性类仅去实现自身所需要的方法, 代码变得更为简洁, 在后续对某个能力进行修改时, 就像修改 save 方法的逻辑那样, 仅仅只需对对应接口的实现类加以修改, 并不会对其他无关的代码产生影响。3. 增强代码灵活性, 适配多种场景, 其中包括第三方模型, 还有自定义模型, 以及不同数据源, 这些场景下都能够借助对应接口迅速进行集成, 无需对原有代码加以修改, 完全契合“开闭原则”, 使得项目更易于扩展。这里有个真实的场景, 某个数据团队, 在运用ISP对模型接口进行了重构之后, 当要新增第三方预训练模型的时候, 不再需要去编写那众多没有用处的空实现了, 只需直接去实现对应方法便能够实现集成, 使得开发效率得到了提升, 提升幅度达到了60%, 并且在后续调试的时候, 报错率下降了, 下降程度为70%。更关键的是, ISP的理念不但适用于这里所涉及的特定情况, 也可应用于别的编程语言, 掌握它, 能够助力你构建起更为科学的代码设计思路, 将原本仅满足于“能运行即可”的状态提升至“编写得优美且维护起来高效”, 而这同样是初级开发者与高级开发者之间核心差异的其中一点。五、互动话题你踩过接口臃肿的坑吗瞧完这篇文章, 确信你已然弄清楚对ISP原则核心的实际操作办法, 并且也认知到它的意义和局限性所在。在评论区交流一下呗: 平常撰写代码时期, 有没有被强制去实现那种根本用不上的方法? 有没有碰到过由于接口繁杂致使在运行的时候出现报错的情况? 你是采用ABC方式, 还是去定义接口?此外, 要是你于实际操作期间碰到运用方面的难题, 又或者不清楚怎样去拆解自身的接口, 同样能够在评论区域留言来一起交流予以解决, 进而共同提高代码质量。