KDnuggets

7 Advanced Python Design Patterns for Data Scientists

8.5内容质量

TL;DR · AI 摘要

数据科学家应掌握七种高级Python设计模式,以应对代码维护和扩展的挑战。文章系统解析Strategy、Adapter等模式的适用场景、Python实现方式及注意事项。

核心要点

  • Strategy模式通过可调用对象实现灵活的评估策略,避免类封装带来的冗余
  • Adapter模式使用Protocol和薄包装器处理不同库的模型接口差异
  • Proxy模式通过延迟加载解决昂贵模型或数据集的提前加载问题

结构提纲

按章节快速跳转。

  1. 揭示共享代码库中函数随时间演化的复杂性问题

  2. 明确模式仅在存在重复变更源时才具有价值

  3. ›Strategy模式

    通过可调用对象实现灵活的评估策略,保持无状态行为

  4. ›Adapter模式

    使用Protocol和包装器解决不同库的接口差异问题

  5. ›Proxy模式

    通过延迟加载优化昂贵资源的初始化时机

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Python设计模式实践
    • 核心模式
      • Strategy
      • Adapter
      • Proxy
    • 设计原则
      • 避免无必要变更源
      • 优先函数而非类封装

金句 / Highlights

值得收藏与分享的关键句。

#Python#设计模式#数据科学#代码架构
打开原文

7 个高级 Python 设计模式,数据科学家必看 - KDnuggets

publ: 2026年10月26日

  • 博客热门文章
  • 主题 AI 职业建议 计算机视觉 数据工程 数据科学 语言模型 机器学习 MLOps NLP 编程 Python SQL
  • 数据集
  • 活动
  • 资源 快速参考指南 推荐 技术简报
  • 广告

订阅电子报

#header end

/ad_wrapper

数据科学家的 7 个高级 Python 设计模式

学习 7 种数据科学代码设计模式,从策略模式和适配器模式到命令模式和代理模式,每个模式都对应解决特定问题,包含 Python 的实现方式以及保持诚实性的注意事项。

作者:

Nahla Davies

,KDnuggets 于 2026 年 10 月 26 日发布在

Python

<div class="addthis_native_toolbox"></div>

大多数训练函数最初都简单易读。但若将其放在共享仓库中使用六个月后,它会积累大量模型特定的条件判断、数据源分支、手动实现的缓存检查和发送到三个不同目的地的日志记录。没有一行代码是错误的。函数只是在每次项目变更时吸收了新的分支,现在每个调用者都继承了整个代码堆。

KDnuggets 已经为中级数据科学家覆盖了 Python 模式,其中管道模式、工厂模式和装饰器模式处理日常情况。本文从该指南结束的地方开始:7 种模式,每种模式对应一种压力,这种压力会在笔记本文件变成他人依赖的代码时出现。

以下所有内容都适用一个节制规则。如果没有任何代码会调用该函数,模式就是形式主义,普通函数仍然是更优工具。只有当模式消除了反复出现的变更源头时,模式才值得使用。

模式 | 重复压力 | Python 形式 | 主要注意事项 --- | --- | --- | --- Strategy | 每个指标或验证策略一个分支 | evaluate(y_true, y_pred, scorer) 接受可调用对象 | 无状态行为应保持为普通函数,而非类 Adapter | 不同库导致的模型 API 和输出形状差异 | 一个本地 Protocol 加上一个重塑输出的薄包装器 | 仅转换接口;语义仍需传递 Template Method | 兄弟实验复制相同的运行顺序 | ABC 拥有 run();子类填充选定钩子 | 继承耦合变体;优先选择策略模式实现自由组合 Observer | 训练循环硬编码到日志记录器和仪表板 | 冻结事件数据类发送到回调列表 | 回调增长时需保持顺序、错误和成本显式 Command | 必须排队、重试或传输的运行 | 带有 ID 和 execute(runner) 的冻结数据类 | 种子、数据和代码版本仍需显式标识符 Facade | 一个任务需要五次导入和一次设置流程 | 一个任务级方法委托给组件 | 保持简洁,否则会成为新的单体 Proxy | 贵重模型或数据集在需要前加载 | 相同 predict() 合约;_load() 在首次使用时运行一次 | 需要决策:失效处理、线程安全性和加载失败

表 1. 所有 7 个模式映射到各自解决的压力、Python 形式以及保持诚实性的注意事项。原始参考表为本文创建。所有 7 种形式在 Python 3.10 上验证。模式集基于经典目录;本文总结了指导原则。

1. 用策略模式交换评估逻辑

一个实验运行器如果通过 if metric == "accuracy" 来选择评分指标,每当团队采用新指标时都需要新增分支。策略模式通过将评分行为作为调用者传入的参数值,消除了这种分支:

code
def evaluate(y_true, y_pred, scorer):
        return scorer(y_true, y_pred)

evaluate(y_true, y_pred, accuracy)          # 0.6
evaluate(y_true, y_pred, recall_pos)        # 0.667

两次调用都针对相同的五组预测结果,但运行器永远不知道执行了哪种策略,这种无知恰恰是其价值所在。现在每个评分器都可以独立测试,新增评分器只需编写函数而非修改实验代码。策略模式与命令模式(Command)经常容易混淆,因此有必要在此划清界限。策略模式当前为调用者提供可互换的相同任务实现方式,而命令模式(Command)则会将操作与其参数打包,以便后续或在其他位置执行。

Python 本身带来了需要注意的限制。无状态的策略应保持为普通可调用对象(如上文的评分器),因为只有当需要配置信息或相关方法与行为绑定时,类才有存在的必要。

2. 用适配器统一模型 API

检查模型来源库的评估代码已经将接口问题泄露到下游。解决方案是定义一个本地契约(typing.Protocol),并为所有不匹配的实现添加薄包装层:

code
class Predictor(Protocol):
        def predict_proba(self, X) -> list: ...

class LegacyAdapter:
        def __init__(self, model):
            self._model = model

        def predict_proba(self, X):
            scores = self._model.score_rows(X)
            return [[1 - s, s] for s in scores]

遗留模型暴露了 score_rows() 方法并返回原始得分,因此适配器将其转换为下游所有组件期望的两列概率格式。一旦存在这个包装层,切换库时就无需重写评估层。但 Protocol 本身也存在警告:其检查是静态的,即使使用 @runtime_checkable 装饰器,isinstance() 也仅检测属性是否存在,这意味着 predict_proba 参数错误的类仍能通过检查。

更深层的危险出现在语义层面。适配器仅转换接口,不会处理其他差异,因此类顺序差异、决策阈值差异或校准/未校准概率差异都会直接穿透适配器。试图掩盖这些差异的适配器,实质是穿着兼容性外衣的缺陷制造者。

3. 用模板方法修复实验框架

相关性较强的实验容易相互复制其编排逻辑,但复制的代码会逐渐发散。模板方法在基于 abc 构建的基类中一次性定义生命周期,允许子类实现真正变化的步骤:

code
class Experiment(ABC):
        def run(self):
            self.load()
            self.fit()
            self.evaluate()
            self.save()

        @abstractmethod
        def fit(self): ...

树模型子类实现fit()方法并继承其余所有内容,因此运行时会按顺序执行load、fit、evaluate和save,每次运行每个变体都会如此。没有人会覆盖run()方法,因为稳定的骨架结构正是设计的核心。过去需要在兄弟实验之间复制的编排逻辑现在只存在于一个地方,而一致的清理和报告功能也随之免费获得。

该模式通过继承获得的灵活性也伴随着继承带来的代价。每个变体仍然与基础生命周期紧密耦合。一旦步骤需要独立更改或自由组合,它们就应该以运行时组合的策略对象形式存在,而不是更深的子类继承树中。

4. 通过观察者广播运行事件

一个训练循环通过名称调用实验追踪器、控制台、仪表板和早停检查,这四个部分有四个各自独立于训练的变更原因。观察者模式反转了依赖关系,使循环只发出一个不可变事件,而回调列表决定后续操作:

code
@dataclass(frozen=True)
class EpochEnded:
        epoch: int
        loss: float

for cb in observers:
        cb(EpochEnded(epoch, loss))

两个职责分离的观察者展示了这种设计的优势。控制台日志记录器将每个事件格式化为一行输出,而最佳分数追踪器则持续记录见过的最低损失值,二者互不知晓彼此存在。后续添加仪表板写入器只需向回调列表追加一个可调用对象,训练循环本身无需任何修改。事件冻结机制比表面看起来更重要,因为任何试图修改事件的观察者都会触发FrozenInstanceError,而不是悄无声息地破坏后续观察者接收到的数据。

观察者模式在规模扩大时会遇到问题。一个包含三个回调的可见列表易于理解,但事件顺序、错误处理和每个事件的成本都需要明确答案,否则这个列表会演变成一个无人能追踪的隐形事件总线。

5. 将可复现运行封装为命令

某些运行不应该在有人想到时立即执行。一个扫描可能需要等待夜间资源,而上周产生有趣结果的运行今天可能值得精确重复。命令模式将请求本身转化为由冻结数据类构建的对象:

code
@dataclass(frozen=True)
class TrainModel:
        dataset_id: str
        model_name: str
        config_id: str

        def execute(self, runner):
            return runner(self)

该对象可以通过asdict()干净地序列化,因此可以被记录、排队、重试或发送到另一台机器的工作者进程。冻结标志保证执行的请求与记录的请求完全一致。运行内容不再依赖于运行时间和地点,这才是真正的关键。但要注意不要让"可复现"这个词承担超出其能力范围的工作。命令捕获的是请求而非环境。数据集版本、代码版本、随机种子和平台假设仍需要在对象内部显式标识,否则重放时可能会在相同名称下生成不同的模型。

6. 通过门面模式隐藏子系统噪声

当在笔记本中评估一个配置需要五次导入和复杂的初始化流程时,每个使用该流水线的消费者都在支付集成税。门面模式将子系统封装到单个任务级方法中:

code
class ExperimentFacade:
        def train_and_report(self, config):
            cleaned = self._preprocessor.clean(config)
            model = self._trainer.fit(cleaned)
            return self._reporter.report(model)

外观模式本身并不执行任何实际工作;它将任务委托给专注的组件,仅负责决定执行顺序,这使其实现仅需三行代码,同时让组件能够独立测试。它也是之前提到的适配器模式的镜像。适配器让一个不兼容的对象适配本地接口,而外观模式则简化了对一个已有完整功能子系统的访问。

调用方因此获得了一个稳定的入口点,而内部实现则可以自由变更。风险在于控制欲。当外观模式开始拥有验证规则、重试策略和状态管理时,它就不再是简单的门面,而变成了新的单体架构,因此要在它还保持精简时及时拆分。

7. 使用代理控制昂贵资源的访问

在导入时加载一个3GB的模型会让每次笔记本重启和每次服务部署都付出可能从未使用过的资源代价。代理模式可以延迟成本产生,同时保持消费者已编码的接口契约:

code
class LazyModelProxy:
        def __init__(self):
            self._model = None

        def predict(self, X):
            if self._model is None:
                self._model = RealModel()
            return self._model.predict(X)

真实模型仅在第一次调用predict()时加载,之后不再重复加载,而消费者继续调用他们熟悉的同名方法,完全不知道存在这个边界。同样的模式也能有效防护昂贵的数据集读取操作。

该模式不会为你做任何运营决策。缓存失效、线程和进程安全、加载失败时的行为都需要在代理实现前明确写出答案。一个静默提供过期资源的代理比显式的缓慢加载更糟糕,因为缓慢加载至少在说实话。

图1. 三种边界模式如何分配工作。适配器重塑单个不兼容对象,外观模式面向整个子系统,代理模式则代表一个昂贵对象并控制对其的访问。原始图表为本文原创。

在命名模式之前命名压力

三个问题区分了有用的模式和令人印象深刻的类图:什么会反复变化?在变化过程中需要保持稳定的契约是什么?抽象本身引入了哪些新的失败模式?回答这些问题后,选择通常会自然浮现。持续变化的行为指向策略模式,接口不匹配指向适配器模式,固定生命周期带有少量可移动步骤的指向模板方法模式。需要多重监听器时选择观察者模式,需要传递工作时使用命令模式,需要隐藏嘈杂子系统时使用外观模式,需要防护昂贵资源时使用代理模式。

所有这些都是代码层面的边界,而围绕它们的规范属于项目层面。关于如何构建数据科学项目的逐步指南,是接下来自然的阅读内容。当压力从代码形态转移到特征存储、训练-服务偏差和模型级联时,MLOps的设计模式将接替对象设计的边界。

成功的衡量标准始终如一。当调用方变得更简单、边界更容易测试时,模式就找到了它的位置。类图从来都不是任何人的交付物。

Nahla Davies 是一位软件开发人员和技术作家。在全职投入技术写作之前,她曾担任一家 Inc. 5,000 排名体验式品牌机构的首席程序员,该机构的客户包括三星、时代华纳、Netflix 和索尼等。

更多相关内容

  • 数据科学家很少使用的高级 Pandas 模式
  • 构建健壮智能代理系统必备的 5 个设计模式
  • 数据科学家进阶之路:Python 中的中级编程模式
  • 数据科学家必备的 10 个高级 Python 技巧
  • Python 中超越 try-except 的 5 种错误处理模式
  • 值得永久收藏的自定义 Python 装饰器模式

<hr class="grey-line"><br> <div><h3>我们推荐的 5 门免费课程</h3><br> </div>

Mailchimp for WordPress v4.14.1 - https://wordpress.org/plugins/mailchimp-for-wp/

/ Mailchimp for WordPress 插件

您可以从此处开始编辑。

如果评论已关闭。

<= 上一篇

#content end

<script type="text/javascript">kda_sid_write(kda_sid_n);</script>

最新文章

  • 数据科学家必备的 7 个高级 Python 设计模式 公司说想要“AI 体验”时真正意味着什么 2026 年你应该尝试的 5 个免费 AI 编码代理 如何使用 ijson 在 Python 中处理大型 JSON 文件 Google 开源 Harness²:无需重新训练即可实现自我优化的 AI 代理 使用 Scikit-LLM 进行命名实体识别

热门文章

  • 开发者必备的 5 个 Docker 容器
  • AI 代理技能必备的 5 个最佳 GitHub 仓库
  • 可替代付费订阅的 7 个开源 AI 工具
  • 2026 年你应该尝试的 5 个免费 AI 编码代理
  • AI 软件克隆的兴起:为什么 REA 和 ArtCraft 正在病毒式传播
  • 2026 年可使用的 5 个免费 LLM API 提供商
  • 使用 Scikit-LLM 进行命名实体识别
  • Google 开源 Harness²:无需重新训练即可实现自我优化的 AI 代理
  • LLM 推理工程的 5 个有趣项目
  • Claude Haiku 5.5:新功能及如何用于快速、低成本的 AI 工作流

#content_wrapper end

© 2026

Guiding Tech Media

|

关于

联系我们

广告合作

隐私政策

服务条款

发布于 2026 年 10 月 26 日

blank

不,谢谢!

/.main_wrapper

<script defer type="text/javascript" src="https://s7.addthis.com/js/300/addthis_widget.js#pubid=gpsaddthis"></script>

noptimize

/noptimize