从“能装上”到“可复现”: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 编程的体验会发生质变:问题更少,发布更稳,协作更轻,代码也更值得信任。

相关推荐
赏金术士1 小时前
Kotlin 习题集 · 基础篇
android·开发语言·kotlin
Agent产品评测局1 小时前
传统RPAvsAI Agent,制造业生产场景能力对比详解 —— 2026智能制造自动化选型全景盘点
人工智能·ai·chatgpt·自动化·制造
jiayong231 小时前
Python面试题集 - 基础语法与核心概念
开发语言·windows·python
元智启1 小时前
企业AI如何开发:从“野生智能体”到“平台化治理”
大数据·人工智能
05候补工程师1 小时前
ROS 2 入门:从零实现小海龟 (Turtlesim) 的手动控制与自动化绘圆
运维·经验分享·python·ubuntu·机器人·自动化
ch.ju1 小时前
Java程序设计(第3版)第三章——数组的遍历
java·开发语言
凯瑟琳.奥古斯特1 小时前
Django Flask FastAPI 三者对比
开发语言·python·django·flask·fastapi
godspeed_lucip1 小时前
LLM和Agent——专题2: LLM as Judge 入门(2)
人工智能·python
沪漂阿龙1 小时前
面试题:激活函数是什么?为什么必须非线性,Sigmoid、ReLU、Softmax 怎么选,一文讲透深度学习高频考点
人工智能·深度学习