从“能装上”到“可复现”:Python 团队如何正确使用 requirements.txt、锁定文件与依赖分组

从"能装上"到"可复现":Python 团队如何正确使用 requirements.txt、锁定文件与依赖分组

团队 Python 项目最常见的混乱,往往不是代码写错,而是环境不一致:A 同事本地能跑,B 同事 pip install -r requirements.txt 后报错;CI 今天通过,明天突然失败;生产环境和开发环境看起来装的是"同一批依赖",行为却不一样。

问题的根源在于:"能装上"只说明依赖解析器找到了一个可用组合;"可复现"要求任何人在指定时间、指定平台、指定 Python 版本下,都能安装出同一个环境。 这两者之间隔着版本范围、传递依赖、平台差异、构建后端、包索引状态和锁定策略。

现代 Python 依赖管理里,可以把三类文件分清楚:

类型 作用 是否应该提交 典型使用场景
pyproject.toml / requirements.in 声明"我需要什么" 人类维护的直接依赖
requirements.txt pip 安装清单,也常被当作扁平锁定结果 通常是 部署、CI、传统 pip 工作流
pylock.toml / uv.lock / poetry.lock 记录完整解析结果 可复现安装、生产部署
依赖分组 区分 prod/dev/test/docs/lint 本地开发、测试、文档、CI

pip 官方文档明确说,requirements 文件是给 pip install 使用的安装项列表,但完整语法与 pip 内部细节紧密绑定,并不是一个面向所有工具的通用标准。(pip.pypa.io) Python Packaging 规范则把依赖说明符定义为 PEP 508 风格:可以指定包名、版本范围、extras、URL、环境标记等。(Python 打包用户指南)


一、requirements.txt:不要把它误当成"万能真相"

很多团队的第一个习惯是:

bash 复制代码
pip freeze > requirements.txt
pip install -r requirements.txt

这在小脚本里能用,但在团队项目中很容易埋雷。pip freeze 记录的是"当前环境已经装了什么",里面可能混入临时调试工具、全局污染包、过期依赖,甚至记录了并非项目真正需要的包。

更合理的思路是:

text 复制代码
人类维护:我直接依赖什么
        ↓
工具解析:所有直接依赖 + 传递依赖
        ↓
锁定输出:给 CI / 生产 / 新同事安装

例如先写一个最小输入文件:

txt 复制代码
# requirements.in
fastapi>=0.110,<1.0
uvicorn[standard]>=0.29,<1.0
pydantic>=2,<3

再生成锁定版:

bash 复制代码
python -m pip install pip-tools
pip-compile --generate-hashes -o requirements.txt requirements.in

生成后的 requirements.txt 可能类似:

txt 复制代码
fastapi==0.115.12 \
    --hash=sha256:...
pydantic==2.11.4 \
    --hash=sha256:...
starlette==0.46.2 \
    --hash=sha256:...
uvicorn==0.34.2 \
    --hash=sha256:...

pip-tools 文档说明,pip-compile 可以从 pyproject.tomlsetup.pysetup.cfgrequirements.in 编译出锁定的 requirements.txt;它还支持 --generate-hashes 生成哈希校验。(pip-tools.readthedocs.io)(pip-tools.readthedocs.io)

一句话原则:

text 复制代码
requirements.in / pyproject.toml:人写
requirements.txt:工具生成

如果团队成员手改生成后的 requirements.txt,后续升级、审计、回滚都会变得困难。


二、锁定文件:可复现环境的核心

"能装上"和"可复现"之间最大的差距,是依赖解析存在很多合法答案。

比如你写:

txt 复制代码
requests>=2.30

今天解析到:

txt 复制代码
requests==2.32.3
urllib3==2.2.2
certifi==2024.8.30

几个月后,可能解析到:

txt 复制代码
requests==2.33.0
urllib3==2.3.0
certifi==2025.x.x

这两次都"能装上",但行为未必完全一致。

真正的锁定文件应该记录:

text 复制代码
直接依赖
传递依赖
精确版本
环境标记
Python 版本约束
平台差异
包来源
文件哈希

Python Packaging 已经有 pylock.toml 规范,它的目标就是描述 Python 环境中可复现安装所需的依赖。(Python 打包用户指南) pip 也已有实验性的 pip lock 命令,但官方说明其生成结果只保证对当前 Python 版本和平台有效。(pip.pypa.io)

所以在实践中可以这样选:

项目类型 推荐方案
传统 pip 项目 requirements.in + pip-compile + requirements.txt
现代应用项目 pyproject.toml + uv.lock / pylock.toml
Python 库 pyproject.toml 声明宽松范围,测试矩阵覆盖多个版本
数据科学项目 锁 Python 版本、锁依赖、锁系统依赖,必要时用容器
生产服务 必须提交锁定文件,CI 和部署都从锁定文件安装

一个更稳的生产安装命令:

bash 复制代码
pip install --require-hashes -r requirements.txt

如果你用 pip-tools,同步环境时不要只用 pip install,更推荐:

bash 复制代码
pip-sync requirements.txt

因为 pip install -r requirements.txt 不会自动删除环境里多余的包;pip-sync 会让虚拟环境精确匹配锁定文件。pip-tools 文档也强调,pip-sync 会安装、升级或卸载必要内容,使环境反映 requirements 文件。(pip-tools.readthedocs.io)


三、依赖分组:别让开发工具污染生产环境

团队项目通常至少有这些依赖:

text 复制代码
生产运行:fastapi、sqlalchemy、redis
测试:pytest、coverage
代码质量:ruff、mypy
文档:mkdocs、sphinx
本地调试:ipython、debugpy

如果全部塞进一个 requirements.txt,生产镜像会变大,安全扫描会变吵,升级风险也更高。

现代写法可以放进 pyproject.toml

toml 复制代码
[project]
name = "team-api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
  "fastapi>=0.110,<1.0",
  "uvicorn[standard]>=0.29,<1.0",
  "pydantic>=2,<3",
]

[project.optional-dependencies]
postgres = [
  "psycopg[binary]>=3.2,<4",
]
redis = [
  "redis>=5,<6",
]

[dependency-groups]
dev = [
  "pytest>=8,<9",
  "coverage[toml]>=7,<8",
  "ruff>=0.8,<1",
  "mypy>=1.13,<2",
]
docs = [
  "mkdocs>=1.6,<2",
]

这里有两个容易混淆的概念:

[project.optional-dependencies] 是面向用户的 extras。

例如你的库支持 PostgreSQL,用户可以这样安装:

bash 复制代码
pip install "team-api[postgres]"

[dependency-groups] 是面向项目内部的开发分组。

例如测试、lint、文档构建、类型检查。Python Packaging 规范说明,依赖组适合 linting、testing 等内部开发用途,也适合不构建分发包的脚本集合;这些依赖组不会作为构建后包的元数据发布。(Python 打包用户指南)

判断标准很简单:

text 复制代码
用户是否需要通过 pip install your-package[xxx] 使用它?
是:optional-dependencies
否:dependency-groups

四、一套可直接落地的团队工作流

推荐目录结构:

text 复制代码
team-api/
├── pyproject.toml
├── requirements/
│   ├── prod.txt
│   ├── dev.txt
│   └── docs.txt
├── src/
├── tests/
└── .github/workflows/ci.yml

1. 固定 Python 版本

bash 复制代码
python --version
# Python 3.12.x

可以在仓库里加:

text 复制代码
# .python-version
3.12

或在 Dockerfile / CI 中明确指定:

yaml 复制代码
strategy:
  matrix:
    python-version: ["3.12"]

2. 生成生产锁定文件

使用 pip-tools

bash 复制代码
pip-compile \
  --generate-hashes \
  -o requirements/prod.txt \
  pyproject.toml

3. 生成开发锁定文件

如果使用分层 requirements,可以让 dev 受 prod 约束:

txt 复制代码
# requirements/dev.in
-c prod.txt
pytest>=8,<9
coverage[toml]>=7,<8
ruff>=0.8,<1
mypy>=1.13,<2

然后:

bash 复制代码
cd requirements
pip-compile --generate-hashes -o dev.txt dev.in

pip-tools 文档也给出了 layered requirements 的思路:开发依赖可以通过 -c requirements.txt 受生产依赖约束,从而保证 dev 和 prod 中共享依赖版本一致。(pip-tools.readthedocs.io)

4. 新同事本地初始化

bash 复制代码
python -m venv .venv
source .venv/bin/activate

python -m pip install -U pip pip-tools
pip-sync requirements/prod.txt requirements/dev.txt

Windows PowerShell:

powershell 复制代码
py -3.12 -m venv .venv
.venv\Scripts\Activate.ps1

python -m pip install -U pip pip-tools
pip-sync requirements/prod.txt requirements/dev.txt

5. CI 中禁止"漂移安装"

yaml 复制代码
name: ci

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: python -m pip install -U pip pip-tools

      - run: pip-sync requirements/prod.txt requirements/dev.txt

      - run: ruff check .

      - run: pytest -q

CI 的关键不是"装上最新依赖",而是"装出和团队约定一致的环境"。


五、为什么"可复现"这么难?

1. 传递依赖会变化

你只写了:

txt 复制代码
fastapi

但实际会装:

text 复制代码
fastapi
├── starlette
├── pydantic
├── typing-extensions
└── anyio

任何一个子依赖发布新版本,都可能影响最终环境。

2. 平台不同,wheel 不同

同一个包在 macOS、Linux、Windows 上可能安装不同 wheel。带 C 扩展的包,如 numpycryptographypsycopg,更容易受到平台、ABI、系统库影响。

3. Python 版本不同,依赖树不同

很多依赖会写环境标记:

txt 复制代码
importlib-metadata; python_version < "3.10"

Python 3.9 和 3.12 解析出来的依赖树可能不同。

4. pip install 不等于同步

假设环境里已经有:

text 复制代码
old-debug-tool==1.0

你运行:

bash 复制代码
pip install -r requirements.txt

它不会主动删除这个多余包。于是测试可能偷偷依赖了一个锁定文件里没有的包。

5. 未锁哈希,供应链风险更高

只锁版本不锁文件哈希,仍然不如锁版本 + 锁哈希可靠。实际生产项目建议尽量开启 hash checking。


六、常见错误与修复方式

错误 1:直接手写巨大 requirements.txt

不推荐:

txt 复制代码
fastapi==0.115.12
starlette==0.46.2
anyio==4.9.0
sniffio==1.3.1
...

更推荐:

txt 复制代码
# requirements.in
fastapi>=0.110,<1.0

然后工具生成完整锁定结果。

错误 2:生产和开发混在一起

不推荐:

txt 复制代码
fastapi
pytest
ruff
mkdocs
ipython
debugpy

推荐拆开:

text 复制代码
requirements/prod.txt
requirements/dev.txt
requirements/docs.txt

错误 3:库项目过度锁死依赖

如果你写的是库,不应该轻易这样:

toml 复制代码
dependencies = [
  "requests==2.32.3",
]

库应该给兼容范围:

toml 复制代码
dependencies = [
  "requests>=2.30,<3",
]

因为库会被别人的应用集成。真正需要锁死的是"最终应用"的运行环境,而不是所有可复用库的公开依赖。

错误 4:升级依赖没有流程

推荐设定固定升级节奏:

bash 复制代码
pip-compile --upgrade -o requirements/prod.txt pyproject.toml
pip-compile --upgrade -o requirements/dev.txt requirements/dev.in
pip-sync requirements/prod.txt requirements/dev.txt
pytest

如果只升级某个包:

bash 复制代码
pip-compile --upgrade-package fastapi -o requirements/prod.txt pyproject.toml

pip-compile 默认会尽量保留现有锁定结果;需要升级全部依赖时使用 --upgrade,升级单个包时使用 --upgrade-package。(pip-tools.readthedocs.io)


七、给团队的一份依赖管理约定

可以直接放进团队 README:

md 复制代码
## Dependency Policy

1. 不直接手改生成后的 requirements/*.txt。
2. 新增运行时依赖,修改 pyproject.toml 的 [project].dependencies。
3. 新增测试、lint、docs 工具,修改 [dependency-groups] 或 requirements/*.in。
4. 变更依赖后必须重新生成锁定文件。
5. 本地和 CI 必须使用 sync,而不是随意 pip install。
6. 生产环境只安装 prod 锁定文件。
7. 每次依赖升级必须跑完整测试。
8. Python 小版本、基础镜像、系统包也属于环境契约。

八、实践结论

requirements.txt 不是过时文件,但它不应该承担所有角色。它适合作为 pip 安装清单,也可以作为由工具生成的锁定结果;但项目的"意图"更适合写在 pyproject.tomlrequirements.in 中。

锁定文件解决的是可复现问题。它把"当前解析器碰巧选了什么"固化下来,让新同事、CI、测试环境和生产环境尽可能一致。

依赖分组解决的是环境边界问题。生产只需要运行依赖,开发才需要测试、lint、类型检查、文档和调试工具。

最终建议可以浓缩成一句 Python 最佳实践:

text 复制代码
声明依赖要清晰,锁定结果要提交,安装环境要同步,依赖升级要审查。

当团队从"我这里能跑"走向"任何人都能复现",Python 编程的体验会发生质变:问题更少,发布更稳,协作更轻,代码也更值得信任。

相关推荐
奔跑中的小象5 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南5 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang6 小时前
LangGraph 状态机实战
人工智能
DLYSB_6 小时前
存储运维实战:基于 Ceph Event 监听与 Python 适配器的分布式存储健康度物理声光响应架构
运维·ceph·python·报警灯
刹那芳华19926 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作6 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门6 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜6 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟6 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦6 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能