使用 import-linter 让你的 Python 项目架构更整洁

对于活跃的大型 Python 项目而言,维持架构的整洁性是一件颇具挑战的事情,这主要体现在包与包、模块与模块之间,难以保持简单而清晰的依赖关系。

一个大型项目,通常包含数以百记的子模块,各自实现特定的功能,互相依赖。如果在架构层面上缺少设计,开发实践上没有约束,这些模块间的依赖关系,常常会发展成为一个胡乱缠绕的线团,让人难以理清头绪。

这会带来以下问题:

架构理解成本高:当新人加入项目时,会有许多关于架构的疑问,比方说:既然 common.validators 是一个低层的通用工具模块,为何要引用高层模块 workloads 中的 ConfigVar 模型?•影响开发效率:想要开发新功能时,开发者难以判断代码应放在哪个包的哪个模块中,而且不同的人可能会有不同的看法,很难达成共识•模块职责混乱:依赖关系很乱,基本等同于模块的职责也很乱,这意味着部分模块可能承担太多,关注了不应该关注的抽象

如果把依赖关系画成一张图,一个架构健康的项目的图,看上去应该层次分明,图中所有依赖都是单向流动,不存在环形依赖。健康的依赖关系,有助于各个模块达成“关注点分离”的状态,维持职责单一。

图片

本文介绍一个治理模块间依赖关系的工具:import-linter[1] 。

import-linter 简介

import-linter[2] 是由 seddonym[3] 开发的一个开源代码 Linter 工具。

要使用 import-linter 检查依赖关系,首先需要在配置文件中定义一些“契约(contract)”。举个例子,下面是一份 import-linter 的配置文件:

其中的 [importlinter:contract:layers-main] 部分,定义了一个名为 the main layers 的“分层(layers)”类型的契约,分层契约意味着高层模块可以随意导入低层模块中的内容,反之就不行。

the main layers 设定了一个分层关系: foo_proj.client 模块属于高层,foo_proj.lib 属于低层。

运行命令 lint-imports,工具会扫描当前项目中的所有 import 语句,构建出模块依赖关系图,并判断依赖关系是否符合配置中的所有契约。

假如在项目中的 foo_proj/lib.py 文件里,存在以下内容:

则会导致 lint-imports 命令报错:

只有当我们删除这条 import 语句后,代码才能通过检查。

除了“分层”类型的契约以外,import-linter 还内置了两种契约:

•禁止(forbidden)[4]:禁止一些模块导入你所指定的其他模块•独立(independence)[5]:标记一个列表中的模块互相独立,不会导入其他模块中的任何内容

如果这些内置契约不能满足你的需求,你还可以编写自定义契约,详情可查阅 官方文档[6]。

在项目中引入 import-linter

要在项目中引入 import-linter 工具,首先需要编写好你所期望的所有契约。你可以试着从以下几个关键问题开始:

从顶层观察项目,它由哪几个关键分层构成,之间的关系如何?许多项目中都存在类似 application -> services -> common -> utils 这种分层结构,将它们记录为对应契约•对于某些复杂的子模块,其内部是否存在清晰的分层?如果能找到 views -> models -> utils 这种分层,将其记录为对应契约•有哪些子模块满足“禁止(forbidden)”或“独立(independence)”契约?如果存在,将其记录下来

将这些契约写入到配置文件中以后,执行 lint-imports,你会看到海量的报错信息(如果没有任何报错,那么恭喜你,项目很整洁,关掉文章去干点其他事情吧!)。它们展示了哪些导入关系违反了你所配置的契约。

逐个分析这些报错信息,将其中不合理的导入关系添加到各契约的 ignore_imports 配置中:

处理完全部的报错信息以后,配置文件中的 ignore_imports 可能会包含上百条必须被忽略的导入信息,此时,再次执行 lint-imports 应该不再输出任何报错(一种虚假的整洁)。

接下来便是重头戏,我们需要真正修复这些导入关系。

饭要一口一口吃,修复依赖关系也需要一条一条来。首先,试着从 ignore_imports 中删除一条记录,然后执行 lint-imports,观察并分析该报错信息,尝试找到最合理的方式修复它。

不断重复这个过程,最后就能完成整个项目的依赖关系治理。

Tip:在删减 ignore_imports 配置的过程中,你会发现有些导入关系会比其他的更难修复,这很正常。修复依赖关系常常是一个漫长的过程,需要整个团队的持续投入。

修复依赖关系的常见方式

下面介绍几种常见的修复依赖关系的方式。

为了方便描述,我们假设在以下所有场景中,项目定义了一个“分层”类型的契约,而低层模块违反契约,反过来依赖了高层模块。

1. 合并与拆分模块

调整依赖关系最直接的办法是合并模块。

假设有一个低层模块 clusters,违规导入了高层模块 resources 的子模块 cluster_utils 里的部分代码。考虑到这些代码本身就和 clusters 有一定关联性,因此,你其实可以把它们直接挪到 clusters.utils 子模块里,从而消除这个依赖关系。

如下所示:

如果被依赖的代码与所有模块间的关联都不太密切,你也可以选择将它拆分成一个新模块。

比方说,一个低层模块 users 依赖了高层模块 marketing 中发送短信相关的代码,违反了契约。你可以选择把代码从 marketing 中拆分出来,置入一个处于更低层级的新模块 utils.messaging 中。

这样做以后,不健康的依赖关系便能得到解决。

2. 依赖注入

依赖注入(Dependency injection)是一种常见的解耦依赖关系的技巧。

举个例子,项目中设置了一个分层契约:marketing -> users, 但 users 模块却直接导入了 marketing 模块中的短信发送器 SmsSender 类,违反了契约。

要通过“依赖注入”修复该问题,我们可以直接删除代码中对 SmsSender 的依赖,改为要求调用方必须在实例化 User 时,主动传入一个“代码通知器(sms_sender)”对象。

这样做以后,User 对“短信通知器”的依赖就变弱了,不再违反分层契约。

添加类型注解

但是,前面的依赖注入方案并不完美。当你想给 sms_sender 参数添加类型注解时,很快会发现自己开始重蹈覆辙:不能写 def __init__(self, sms_sender: SmsSender),那样得把删掉的 import 语句找回来。

即使像上面这样,把 import 语句放在 TYPE_CHECKING 分支中,import-linter 仍会将其当做普通导入对待(注:该行为可能会在未来发生改动,详见 Add support for detecting whether an import is only made during type checking · Issue #64[7]),将其视为对契约的一种违反。

为了让类型注解正常工作,我们需要在 users 模块中引入一个新的抽象:SmsSenderProtocol 协议(Protocol),替代实体 SmsSender 类型。

这样便解决了类型注解的问题。

Tip:通过引入 Protocol 来解耦依赖关系,其实上是对依赖倒置原则(Dependency Inversion Principle)的一种应用。依赖倒置原则认为:高层模块不应该依赖低层模块,二者都应该依赖抽象。

关于它的更多介绍,推荐阅读我的另一篇文章:《Python 工匠:写好面向对象代码的原则(下) 》[8]。

3. 简化依赖数据类型

在以下代码中,低层模块 monitoring 依赖了高层模块 processes 中的 ProcService 类型:

经过分析后,可以发现 build_monitor_config 函数实际上只使用了 service 对象的两个字段:host 和 port,不依赖它的任何其他属性和方法。所以,我们完全可以调整函数签名,将其改为仅接受两个必要的简单参数:

调用方的代码也需要进行相应修改:

通过简化函数所接收的参数类型,我们消除了模块间的不合理依赖。

4. 延迟提供函数实现

Python 是一门非常动态的编程语言,我们可以利用这种动态,延迟提供某些函数的具体实现,从而扭转模块间的依赖关系。

假设低层模块 users 目前违反了契约,直接依赖了高层模块 marketing 中的 send_sms 函数。要扭转该依赖关系,第一步是在低层模块 users 中定义一个用来保存函数的全局变量,并提供一个配置入口。

代码如下所示:

调用 send_sms 函数时,判断当前是否已提供具体实现:

完成以上修改后,users 不再需要从 marketing 中导入“短信发送器”的具体实现。而是可以由高层模块 marketing 来一波“反向操作”,主动调用 set_send_sms_func,将实现注入到低层模块 users 中:

这样便完成了依赖关系的扭转。

变种:简单的插件机制

除了用一个全局变量来保存函数的具体实现以外,你还可以将 API 设计得更复杂一些,实现一种类似于“插件”的注册与调用机制,满足更丰富的需求场景。

举个简单的例子,在低层模块中,实现“插件”的抽象定义以及用来保存具体插件的注册器:

在其他模块中,调用 SmsSenderPluginCenter.register 来注册具体的插件实现:

和使用全局变量一样,插件机制同样是对依赖倒置原则的一种具体应用。上面的代码仅包含最简单的原理示意,真实的代码实现会更复杂一些,不在此文中赘述。

5. 由配置文件驱动

假设低层模块 users 违规依赖了高层模块 marketing 中的一个工具函数 send_sms。除了使用上面介绍的方式以外,我们也可以选择将工具函数的导入路径定义成一个配置项,置入配置文件中。

在 users 模块中,不再直接引用 marketing 模块,而是通过动态导入配置中的工具函数的方式,来使用 send_sms 函数。

这样也可以完成依赖关系的解耦。

Tip:关于 import_string 函数的具体实现,可以参考 Django 框架[9]。

6. 用事件驱动代替函数调用

对于那些耦合关系本身较弱的模块,你也可以选择用事件驱动的方式来代替函数调用。

举个例子,低层模块 networking 每次变独立域名数据时,均需要调用高层模块 applications 中的 deploy_networking 函数,更新对应的资源,这违反了分层契约。

该问题很适合用事件驱动来解决(以下代码基于 Django 框架的信号机制[10] 编写)。

引入事件驱动的第一步是发送事件。我们需要修改 networking 模块,删除其中的函数调用代码,改为发送一个类型为 custom_domain_updated 的信号:

第二步,是在 applications 模块中新增事件监听代码,完成资源更新操作:

这样便完成了解耦工作。

总结

在依赖关系治理方面,import-linter[11] 是一个非常有用的工具。它通过提供各种类型的“契约”,让我们得以将项目内隐式的复杂依赖关系,通过配置文件显式的表达出来。再配合 CI 等工程实践,能有效地帮助我们维持项目架构的整洁。

如果你想在项目中引入 import-linter,最重要的工作是修复已有的不合理的依赖关系。常见的修复方式包括合并与拆分、依赖注入、事件驱动,等等。虽然手法多种多样,但最重要的事用一句话便可概括:把每行代码放在最恰当的模块中,必要时在当前模块引入新的抽象,借助它的力量来反转模块间的依赖关系。

愿你的项目架构永远保持整洁!

References

[1] import-linter: https://github.com/seddonym/import-linter
[2] import-linter: https://github.com/seddonym/import-linter
[3] seddonym: https://github.com/seddonym
[4] 禁止(forbidden): https://import-linter.readthedocs.io/en/stable/contract_types.html#forbidden-modules
[5] 独立(independence): https://import-linter.readthedocs.io/en/stable/contract_types.html#independence
[6] 官方文档: https://import-linter.readthedocs.io/en/stable/custom_contract_types.html
[7] Add support for detecting whether an import is only made during type checking · Issue #64: https://github.com/seddonym/grimp/issues/64
[8] 《Python 工匠:写好面向对象代码的原则(下) 》: https://www.piglei.com/articles/write-solid-python-codes-part-3/
[9] Django 框架: https://github.com/django/django/blob/main/django/utils/module_loading.py#L19
[10] Django 框架的信号机制: https://docs.djangoproject.com/en/4.2/topics/signals/
[11] import-linter: https://github.com/seddonym/import-linter

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1523489.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【路径规划】在MATLAB中使用粒子群优化(PSO)进行最优移动机器人路径规划

摘要 本文介绍了使用粒子群优化(Particle Swarm Optimization, PSO)算法实现移动机器人的路径规划。PSO是一种基于群体智能的优化算法,通过模拟粒子群体在搜索空间中的迭代更新,找到全局最优路径。本文通过MATLAB仿真展示了PSO在…

python-小理帮老师改错

题目描述 老师给小理发了一封电子邮件,任务如下。 写一个程序,给你 n 个数,输出 X。 Xnum1^p1​​num2^p2​​⋯numn^pn​​ num1​,num2​,⋯⋯,numn​ 都是整数,p1​,p2​&#xf…

重大突破 谷歌DeepMind展示GenRM技术:AI推理能力的新里程碑

引言: 近日,谷歌DeepMind团队在arxiv平台上发表了一项突破性论文,正式推出了GenRM技术,这一创新成果显著提升了AI在复杂任务处理中的能力表现,再次跨越了技术界限,为人工智能的推理能力树立了崭新的标杆。 …

HMI触屏网关-VISION如何和OPC UA服务端通信

上文:HMI触屏网关-VISION如何与Node-red通信-CSDN博客 1. 准备工作 1.1. 创建OPC UA服务端 在与OPC UA服务端进行通信时,首先要确认服务端已就绪,本示例使用辅助软件1模拟OPC UA服务端。 1.2. 创建模拟点位 1.3. 测试通信 辅助软件2&…

【C语言从不挂科到高绩点】10-C语言中数组 01

Hello!彦祖们,俺又回来了!!!,继续给大家分享 《C语言从不挂科到高绩点》课程!! 本节课开始重点给大家讲讲C语言中的数组 本套课程将会从0基础讲解C语言核心技术,适合人群: 大学中开…

苹果手机显示“连接iTunes”是什么意思?

在日常使用苹果手机的过程中,有时我们可能会遇到屏幕突然显示“连接iTunes”的提示,这让不少用户感到困惑和不安。今天,我们就来深度解析一下这个提示的含义,并为大家提供详细的解决方案。 一、显示“连接iTunes”的含义 当苹果…

CasaOS系统本地安装Gopeed高速下载软件并实现异地远程访问下载文件

文章目录 前言1. 更新应用中心2.Gopeed安装与配置3. 本地下载测试4. 安装内网穿透工具5. 配置公网地址6. 配置固定公网地址 前言 本文主要介绍如何在轻NAS系统CasaOS小主机中安装支持全平台的高速下载器Gopeed,并结合内网穿透工具配置公网地址实现远程访问本地主机…

Nginx部署前端vue项目操作步骤和方法~小皮

部署前端Vue.js项目到Nginx上,是开发流程中至关重要的一步,它意味着将静态文件托管在Web服务器上,使应用程序能够被用户访问和交互。下面将详细介绍如何使用Nginx部署前端Vue项目的操作步骤和方法: 准备构建Vue项目 安装Node.js和…

在BrowserStack上进行自动化爬虫测试的终极指南

一、背景介绍 随着互联网的快速发展,数据变得越来越宝贵,爬虫技术已成为从网页中提取信息的重要工具。然而,在不同的环境中测试和运行爬虫脚本可能会带来挑战。尤其是在多浏览器、多平台的环境中确保爬虫的稳定性和兼容性是一个令人头疼的问…

HTTP 之 消息结构(二十二)

HTTP(超文本传输协议)是一种用于传输超媒体文档的协议,它定义了客户端和服务器之间请求和响应的消息结构。HTTP消息由一系列标准头部字段、一个空行和可选的消息体组成。 客户端请求消息 请求消息包括以下格式:请求行(…

Flask_admin—快速搭建访客登记系统Web管理后台

简介:在《App Inventor 2—自制身份证识别及人证比对验证系统》和《MySQL—访客登记系统数据库及Web服务搭建》的基础上,通过在云服务器上的Python程序中使用Flask_admin扩展,快速搭建数据库Web管理后台。通过整合上述实验,了解全…

希尔排序的图解展示与实现

什么是希尔排序 对整个数组进行预排序,即分组排序:按间距为gap分为一组,分组进行插入排序。 预排序的作用与特点 大的数更快地到后面,小的数更快地到前面; gap越大,跳得越快,排完接近有序慢&…

电脑浏览器显示代理服务器拒绝连接的解决办法

问题: 打开电脑浏览器显示代理服务器拒绝连接 解决办法: 1、按住winq键,输入代理,出现更改代理设置 2、将下面的自动检测设置、使用设置脚本、使用代理服务器都设置为关闭,刷新网页即可

人工智能 | 大语言模型应用框架介绍

简介 大语言模型的英文全称为:Large Language Model,缩写为 LLM,也被称为大型语言模型,主要指的是在大规模文本语料上训练、包含百亿级别参数的语言模型,它用来做自然语言相关任务的深度学习模型。 自然语言的相关任…

【数学建模国赛赛前必看】参赛作品及MD5码提交流程

国赛参赛人数非常多,导致了很多时候我们没有办法正常的去上传论文,所以国赛就会有一个MD5码的上传过程,MD5码上传在国赛比赛当中是非常重要的。每年几乎都有5%左右的队伍会因为MD5码上传失败导致最终的论文交稿失败。所以我们今天具体的讲一下…

qt对象析构顺序记录

说明qt对象树 对象析构顺序为: 本对象的析构函数栈成员对象树中自己的孩子们对象树中自己的孙子们 所以,千万别在孩子对象中(qt对象树特有的这个连带析构关系)去访问父对象的任何栈成员(包括堆成员)的信息…

大模型产品经理学习路线,2024最新,从零基础入门到精通,非常详细收藏我这一篇

随着人工智能技术的发展,尤其是大模型(Large Model)的兴起,越来越多的企业开始重视这一领域的投入。作为大模型产品经理,你需要具备一系列跨学科的知识和技能,以便有效地推动产品的开发、优化和市场化。以下…

Unity | 内存优化之资源冗余问题

目录 一、资源冗余 1.主动打包和被动打包 2.依赖资源处理 (1)分别制作AB包,会造成冗余 (2)资源冗余解决办法: (2.1)先主动打依赖资源AB包 (2.2)将两个…

智能分拣投递机器人

产品介绍 自研智能分拣投递机器人,专注于物流行业“NC小件”的分拣与投递,机器人运行稳定、分拣效率高,搭配智能分拣投递系统单台机器人最大作业效率可达400件/H,投递效率相较于传统“小黄人“提升了30%-50%,可替代“…

机器学习之监督学习(一)线性回归、多项式回归、算法优化[巨详细笔记]

机器学习之监督学习(一)线性回归、多项式回归、算法优化 1.监督学习定义2.监督学习分类2.1回归 regression2.2 分类 classification 3.线性回归 linear regression3.1 单特征线性回归模块一:梯度下降 3.2 多特征线性回归模块二:正…