适用读者:被"我这儿能跑、CI 挂了""上个月还能装,今天装不上"折腾过的人。
前置:S10-B03。
实测环境:Python 3.12.3 + pip/venv(实测两次安装的一致性)。
覆盖旧文:—(E 驱动;B03 的导出命令在此工程化)
覆盖扩展:E-S10-008(pip freezevspip-compile)、E-S10-009(--require-hashes)、E-S10-010(Python 版本锁定)。
上一篇:S10-B05 · Flask 路由与错误页
下一篇:S10-J02 · Flask ↔ FastAPI 迁移。
1. 一句话结论
"能跑"是偶然,"可复现"是设计。 可复现依赖三件事,缺一不可:
| # | 要素 | 缺失后果 |
|---|---|---|
| 1 | Python 版本锁定 | 3.12 与 3.11 行为差异(如 tomllib) |
| 2 | 精确版本(含传递依赖) | 上游发了新版 → 明天装出来就不一样 |
| 3 | 哈希校验(可选但推荐) | 供应链投毒(包被替换) |
2. 实测:pip freeze 到底锁了什么
$ pip freeze | head -5
anyio==4.16.0
certifi==2026.10.23
h11==0.16.0
httpcore==1.0.9
httpx==0.27.2
这里只有 5 行,但你只装了 1 个包(httpx)——其余是传递依赖。这就是精确锁定的价值:它把"间接依赖"也钉死。
| 文件 | 内容 | 适合 |
|---|---|---|
requirements.in(手写) |
只写直接依赖(httpx>=0.27) |
人维护 |
requirements.txt(生成) |
完整依赖树 + 精确版本 | 部署/CI |
environment.yml |
conda 环境(含解释器版本) | conda 用户 |
pip-compile(E-S10-008):从 .in 生成 .txt,并保留"为什么需要这个包"的注释:
# requirements.in
httpx>=0.27
# 生成后(pip-compile)
anyio==4.16.0
# via httpx
httpx==0.27.2
# via -r requirements.in
判据:pip freeze 适合"事后快照",pip-compile 适合"长期维护"——因为你能看出每个包的来源。
3. 哈希校验:防"包被替换"(E-S10-009)
# 由 pip-compile --generate-hashes 生成
httpx==0.27.2 \
--hash=sha256:aaaa... \
--hash=sha256:bbbb...
pip install --require-hashes -r requirements.txt
| 收益 | 代价 |
|---|---|
| 包内容被篡改 → 安装失败 | 每次加依赖都要重新生成哈希 |
| 供应链攻击面大幅缩小 | 平台差异(wheel 不同哈希)要全部列出 |
判据:对外分发的服务/库 → 开;内部脚本 → 可不开。
4. Python 版本锁定(E-S10-010)
| 方式 | 写法 |
|---|---|
environment.yml |
dependencies: [python=3.12] |
pyproject.toml |
requires-python = ">=3.12,<3.13" |
.python-version(pyenv/asdf) |
3.12.3 |
| Docker | FROM python:3.12.3-slim |
| CI | actions/setup-python@v5 with: python-version: 3.12.3 |
最小集合:pyproject.toml 的 requires-python + CI 的显式版本 + Docker 镜像 tag(不要用 latest)。
5. 一次"环境漂移"的复现
场景:requirements.txt 里写的是 httpx(无版本)
Day 1 安装 → httpx 0.27.2(当时最新)
Day 30 重装 → httpx 0.28.x(上游发版) → 行为变化 → CI 红
验证方法(可复跑):
# 用宽松约束装一次,记录版本
pip install "httpx" && pip freeze | grep httpx
# 换成精确约束,再装一次
pip install "httpx==0.27.2" && pip freeze | grep httpx
结论:只要依赖文件里出现"没有 == 的行",就存在漂移风险。
6. 五个误区
| 误区 | 真相 |
|---|---|
"pip freeze 就是锁定" |
它锁的是"当前机器上解析出的树",换平台可能不同 |
| "锁了直接依赖就够了" | 传递依赖同样会漂 |
| "哈希校验太麻烦" | 一次配置,长期收益;对外分发尤其值得 |
| "Docker 里不用锁版本" | 镜像重建时 latest 会变 |
| "conda 与 pip 可以任意混" | 顺序(先 conda 后 pip)与冲突需管理 |
7. 自检
- 为什么"能跑"不等于"可复现"?
pip freeze与pip-compile的核心差别是什么?- 哈希校验防的是什么攻击?代价是什么?
- Python 版本锁定有哪四种常见方式?
- 怎么快速判断一个 requirements 文件存在漂移风险?
8. 下一篇
S10-J02 · Flask ↔ FastAPI:迁移与对照
依赖稳定之后,下一篇处理"技术栈变化":同一接口在两种框架下的写法差异、迁移清单,以及那个会影响前端的 404/422 差异。
本篇属于系列《S10 · Python 语言与环境》。扩展点 E-S10-008/009/010。pip freeze 与重建一致性为 2026-11-05 实测。