浅谈大型项目的模块化开发(Based on Python)
写在前面
最近在进行大型项目开发时,自行实现的一些公共模块,包括工具库、API 客户端等,都被扔在一个文件夹下,需要时由该项目的控制代码进行调用,不过因为水平之菜,经过测试没什么问题的代码 PR 上去之后经常会出现需要反复修改的情况。而且另一方面,这些公共模块具有一定的可移植性,但只被一个项目使用,没什么性价比,肯定是需要进一步考虑能够在多个项目实现复用的。
一种好的管理方式(至少是大部分大型开源项目都在使用的方式)是将公共模块单独管理,模块可以独立更新,不必和主项目的提交历史绑在一起,也利好多人协作开发。具体方法包括 Git 的 Submodule 和打包为 Python Package 进行导入两种。
一、Git Submodule
1.1 简介
Git submodule 是 git 原生引用和管理第三方资源的方法。
假如你的项目结构如下:
Main Project└── src/└── module/ └── module_1/如果用 Git submodule 实现对 module_1 的管理,那么主项目 Git 仓库显然作为父仓库 (Superproject),而 module_1 就作为子模块 (Submodule),父仓库只记录子模块的 Git 仓库地址 和当前的 提交哈希,不会直接存储子模块的代码历史。
1.2 操作步骤
食用方法也很简单,子模块和父模块都先建立一个单独的 Git 仓库,git add/commit/push 这些我就不细说了,现在假设两者的仓库都已经建立了,接下来就是 git submodule 的操作了:
- 在父仓库中添加子模块:
git submodule add <子模块的仓库地址> <子模块在父模块中的相对路径>e.g.
git submodule add https://github.com/user/module_1.git module/module_1
# 等价于git submodule add https://github.com/user/module_1.git module/那么父仓库具体是如何记录这一信息的呢?在执行上述操作后,可以看到在父仓库根目录会生成一个 .gitmodules 文件,内容形如:
[submodule "module/module_1"] path = module/module_1 url = https://github.com/user/module_1.git也可以通过以下命令查看子模块信息:
git submodule statusgit submodule foreach "git status"- 初始化子模块:当协作者或其他用户克隆你的父仓库,即主项目时,默认子模块的目录是空的,需要手动拉取子模块代码:
git submodule initgit submodule update当然,Git 也提供了在克隆时就一步到位的选项:
git clone --recurse-submodules <父仓库地址>- 更新子模块:当子模块仓库有新提交时,你可以进入子模块目录拉取最新代码(当然,回退版本也同理):
cd module/module_1git pull origin main
# 如果是回退,则执行git checkout <子模块需要回退到的某个commit或分支>我们前面提到过,两者的提交历史是独立的,父仓库只记录子模块的当前提交哈希,子模块仓库的更新并不会同步到父仓库中,所以父仓库还需要手动提交:
cd ../..git add module/module_1git commit -m "Update submodule module_1 to latest version"这时候,父仓库记录的才是子模块的最新提交哈希。
- 删除子模块:删除步骤稍复杂,需要手动清理:
git submodule deinit -f module/module_1git rm -f module/module_1rm -rf .git/modules/module/module_11.3 好处
Git Submodule 将公共库抽离成单独 Git 仓库,父项目通过 Submodule 管理这些模块,保持版本可控。如需自动拉取子模块代码,也可以在 Github Action 等 CI/CD 工具中执行:
git submodule update --init --recursive从而自动进行构建。
二、Python Package
2.1 简介
Python Package 也是一种常见方式,经常会用到的 Numpy 等就是 Package。这些第三方库能直接通过 pip 安装,分发、复用于各种项目中,虽然你用起来大道至简,但是这是 Package 的维护者在背后为你负重前行,也就是“打包”的过程,具体的打包过程可以从内到外分为三层,下图源自 Python Packaging 的官方文档,非常直观:

- .py 独立模块(Standalone Modules):可以直接导入,但无法通过 pip install 方式分发;
- sdist 源码分发包(Source Distribution Package):通过
setup.py、pyproject.toml等安装元数据,打包成压缩文件(.tar.gz、.zip); - wheel 预编译包(Built Distribution Package):属于 binary package(二进制分发),简单来说就是预编译好的轮子,可以直接安装,例如
pip install my_package.whl;
2.2 操作步骤
以下以一个简单的例子为例,简单过一下打包步骤:
假如原始的代码结构如下:
my_package/ └── src/ └── module_a/ ├── __init__.py └── module_a.py └── module_b/ ├── __init__.py └── module_b.py- 准备项目信息:如果 Package 想要发布,还需要添加一些基本的信息,一般通过
setup.py或pyproject.toml;
my_project/├── LICENSE # 授权├── setup.py # 安装设定├── pyproject.toml # 安装设定├── README.md├── src/ # 包源码 └── my_package/ ├── __init__.py └── ...└── test/ # 测试程序- 编写元数据:一个基本的
setup.py和pyproject.toml示例如下:
from setuptools import setup, find_packages
setup( name="my_package", version="0.1.0", description="A useful Python package", author="Your Name", author_email="you@example.com", packages=find_packages(where="src"), package_dir={"": "src"}, install_requires=[ "numpy>=1.19.0", "requests" ], python_requires='>=3.7',)[build-system]requires = ["setuptools>=61.0"]build-backend = "setuptools.build_meta"
[project]name = "my_package"version = "0.1.0"authors = [ { name="Your Name", email="you@example.com" },]description = "A useful Python package"readme = "README.md"requires-python = ">=3.7"classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent",]基本要素是类似的,也可以混用,现代打包一般采用后者,当然也有很多其他工具,这两种属于是最主流的了。
- 构建包:这一步是可选的,但是一般如果想要发布都会进行 build,提供更快速的安装;
python -m build2.3 发布到 PyPI
PyPI,即 Python Package Index,是 Python 官方的第三方软件存储库,账号注册就不赘述了,下面简单讲一下上传步骤:
pip install twinetwine upload dist/*上传成功后,别人就可以直接使用 pip install my_package 来安装你发布的 Package 了。
很多 Package 一般都自动构建并上传,你可以随便找一个开源项目并在里面看到:
.github/└── workflows/ └── publish.yml完全可以学学它们的 publish.yml 以及项目结构,这里给出一个简单的示例:
name: Publish Python Package
on: push: tags: - 'v*' # 当你创建 v1.0.0 这种 tag 时自动触发
jobs: build-and-publish: runs-on: ubuntu-latest
steps: - name: Checkout code uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11'
- name: Install build dependencies run: | python -m pip install --upgrade pip pip install build twine
- name: Build package run: python -m build
- name: Publish to PyPI env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | twine upload dist/*Token 需要你自行在 PyPI 中获取并在 Github 中的项目仓库 Settings Secrets and variables Actions 点击 New repository secret ,创建 PYPI_USERNAME 和 PYPI_PASSWORD,注意千万不要显式写在工作流中!
完成后,当提交代码并推送 Tag 时:
git tag v1.0.0git push origin v1.0.0GitHub Actions 会自动运行 workflow,构建 wheel 和 sdist 包,并上传到 PyPI。
也可以替换为:
on: release: types: [published]这样就是在发布 Release 后自动触发。
参考资料
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!



