Python 项目如何做依赖管理和可重复构建?
简化版
Python 依赖管理要区分直接依赖和锁定依赖,保证不同环境安装到一致版本。常见方案有 requirements.txt、pip-tools、Poetry、uv 等;工程上要固定 Python 版本、提交锁文件、隔离虚拟环境,并在 CI 中验证可重复安装。
详细版
依赖管理关注几个问题:
- 项目需要哪些直接依赖;
- 间接依赖版本如何锁定;
- 开发、测试、生产依赖如何区分;
- Python 版本如何约束;
- 构建环境是否可重复;
- 依赖是否存在安全漏洞。
常见方式:
- 简单项目:
requirements.txt; - 更可控:pip-tools 的
requirements.in+requirements.txt; - 包管理和发布:Poetry 或现代
pyproject.toml; - 追求速度和锁定体验:uv。
面试重点是:只写一个不带版本的 requirements,很难保证可重复构建。
完整版教学
一、为什么 Python 依赖管理容易出问题
Python 生态非常灵活,但灵活也带来环境不一致问题。开发者本地安装的是某个版本,CI 安装的是另一个版本,生产镜像又因为缓存拿到第三个版本,问题就会很隐蔽。
例如你写:
fastapi
sqlalchemy
这没有锁定版本。今天安装可能是一个版本,几个月后重新安装可能得到新版本。新版本可能改了行为,项目就可能突然失败。
所以依赖管理的核心目标是可重复:同一份代码在不同机器、不同时间安装,应尽量得到一致依赖。
二、直接依赖和间接依赖
直接依赖是你项目明确使用的库,例如 FastAPI、SQLAlchemy、pytest。间接依赖是这些库依赖的库,例如 Starlette、Pydantic、anyio。
只管理直接依赖是不够的,因为间接依赖变化也可能影响项目。锁文件的价值就在于记录完整依赖树。
比如 pip-tools 的做法是:requirements.in 写直接依赖,生成的 requirements.txt 锁定完整版本。
# requirements.in
fastapi
sqlalchemy
生成后可能得到:
fastapi==0.x.x
pydantic==x.x.x
starlette==x.x.x
sqlalchemy==x.x.x
生产安装使用锁定后的文件。
三、requirements.txt、pip-tools、Poetry 怎么选
requirements.txt 简单直接,适合小项目。但如果手动维护完整依赖树,容易混乱。
pip-tools 比较适合想保留 pip 工作流,又希望锁定依赖的团队。它用 pip-compile 生成锁定文件,用 pip-sync 同步环境。
Poetry 提供项目元数据、依赖解析、虚拟环境、构建发布和 lock 文件,适合库项目或较规范的应用项目。
uv 是较新的工具,速度快,支持依赖解析、锁定和虚拟环境管理,越来越多团队在尝试。
面试回答不要只背工具名,要说清楚目标:固定依赖树、隔离环境、保证 CI 和生产可重复。
四、pyproject.toml 的意义
现代 Python 项目越来越多使用 pyproject.toml。它可以集中描述项目元数据、构建系统、依赖、工具配置。例如 black、ruff、mypy、pytest 都可以在里面放配置。
这比散落多个配置文件更统一。对于可发布包,pyproject.toml 也是标准化构建入口。
工程中常见结构:
pyproject.toml
poetry.lock / uv.lock
src/
tests/
这类结构让项目更容易被 CI、构建工具和 IDE 识别。
五、虚拟环境和 Python 版本
依赖管理还必须包含 Python 版本管理。某些库只支持 Python 3.10+,某些语法只在 Python 3.11 可用。如果不约束版本,本地和生产可能出现语法或依赖兼容问题。
可以在 pyproject.toml 中声明:
requires-python = ">=3.11,<3.13"
虚拟环境用于隔离项目依赖,避免全局 Python 被污染。无论用 venv、Poetry 还是 uv,本质都是给项目一个独立环境。
六、生产构建要注意什么
生产镜像应该使用锁定依赖安装,不要每次解析最新版本。构建过程中要尽量可缓存、可复现。
还要区分开发依赖和生产依赖。pytest、black、mypy 这类工具通常不需要进入生产运行环境。生产镜像越小,攻击面和构建时间越可控。
依赖安全扫描也很重要。可以在 CI 中使用 pip-audit、Safety 或平台安全扫描检查已知漏洞。
七、常见误区与追问
| 文件或配置 | 记录内容 | 工程意义 |
|---|---|---|
requirements.in | 直接依赖 | 人维护项目真正需要的库 |
requirements.txt | 完整锁定依赖树 | CI/生产按固定版本安装 |
pyproject.toml | 项目元数据和工具配置 | 统一构建、依赖、测试工具入口 |
| lock 文件 | 解析后的版本图 | 保证不同机器重复安装 |
只写 fastapi:
今天可能解析到 fastapi A + pydantic X
3 个月后可能解析到 fastapi B + pydantic Y
如果没有锁文件,同一份代码构建结果可能不同。
易错点:直接依赖回答“项目想要什么”,锁文件回答“最终实际装什么”。
第一个误区是不提交锁文件。团队成员和 CI 解析出的版本可能不同。
第二个误区是依赖版本完全不限制。上游库升级可能带来破坏性变化。
第三个误区是把开发依赖装进生产镜像。镜像变大,也增加安全风险。
第四个误区是只在本地验证安装。真正要在 CI 中从零安装一次,才能证明依赖声明完整。
- 误区:
requirements.txt里不写版本也算依赖管理。 不锁版本会让安装结果随时间变化,可重复构建无法保证。 - 误区:只锁直接依赖就够了。 间接依赖也可能破坏项目,锁文件要记录完整依赖树。
- 误区:开发依赖可以直接进生产镜像。 pytest、black、mypy 等工具会增加镜像体积和攻击面,生产依赖应尽量精简。
- 追问:pip-tools 和 Poetry 的差别怎么答? pip-tools 保留 pip 工作流,用输入文件生成锁定 requirements;Poetry 更偏项目管理,包含依赖解析、虚拟环境、构建发布和 lock。
- 追问:为什么 CI 要从零安装依赖? 只有干净环境安装成功,才能证明依赖声明完整,不依赖开发者本地缓存或隐藏包。
- 追问:Python 版本为什么也要锁定范围? 语法、标准库行为、二进制包兼容性都和 Python 版本有关,只锁依赖不锁解释器仍可能不可重复。
八、加强记忆
记 Python 依赖管理:直接依赖说明“我需要什么”,锁文件说明“实际安装什么”。工程上要固定 Python 版本、隔离虚拟环境、提交锁文件、区分开发和生产依赖,并在 CI 中验证可重复构建。