4、锁文件(Lock File)的奥秘:pip的requirements.txt vs pipenv的Pipfile.lock,poetry的poetry.lock
聊到依赖管理,锁文件是个绕不开的话题。
我刚开始用Python那会儿,项目依赖全靠一个requirements.txt撑着。当时觉得挺够用的,直到有一次线上环境炸了——同一个项目,开发机跑得好好的,部署到服务器就报错。查了半天,发现是某个依赖库的小版本升级了,接口变了。
嗯,从那以后我才真正意识到:光声明依赖还不够,你得把依赖的“快照”也锁住。
为什么需要锁文件?
说白了,requirements.txt里写的requests>=2.25.0,它只告诉你要装什么版本范围的包。但具体装哪个小版本?子依赖又装哪个版本?这些都没说死。
你想想看,今天装的是2.25.1,明天可能就变成2.26.0了。如果新版本有破坏性变更,你的项目就跟着遭殃。
锁文件的作用,就是把整个依赖树的精确版本都固定下来。谁依赖谁、什么版本、从哪里下载,全都记录得清清楚楚。
requirements.txt是“声明文件”,告诉你要什么。锁文件是“合同文件”,告诉你必须用什么。
pip的requirements.txt——最朴素的方案
传统的requirements.txt其实也能当锁文件用。我见过不少团队的做法是:
# 生成锁定的版本
pip freeze > requirements.txt
# 输出示例
requests==2.28.1
urllib3==1.26.12
certifi==2022.9.24
这样确实把版本锁住了。但问题在于:它只锁顶层依赖,不锁子依赖的依赖关系。而且pip freeze会把当前环境里所有包都列出来,包括那些你根本没用到的。
我曾经在一个老项目里看到过长达300行的requirements.txt,里面一半的包都不知道是干嘛用的。维护起来简直噩梦。
pip freeze生成的锁文件,在不同操作系统或Python版本下可能不一样。跨平台部署时容易出问题。
pipenv的Pipfile.lock——更完整的依赖图谱
pipenv的出现,算是把Node.js的npm那套思路搬到了Python里。它有两个文件:
Pipfile:声明你的直接依赖(类似requirements.txt)Pipfile.lock:完整的依赖锁文件
我打开过一个Pipfile.lock看过,结构大概是这样的:
{
"_meta": {
"hash": {
"sha256": "abc123..."
},
"pipfile-spec": 6
},
"default": {
"requests": {
"version": "==2.28.1",
"hashes": [
"sha256:xxx..."
],
"dependencies": {
"urllib3": ">=1.21.1,<1.27",
"certifi": ">=2017.4.17"
}
}
},
"develop": {
"pytest": {
"version": "==7.1.3"
}
}
}
你看,它不光记录了版本,还记录了哈希值和子依赖关系。哈希值用来校验下载的包是否被篡改,子依赖关系则保证了依赖树的完整性。
我个人习惯用pipenv管理一些中小型项目。它最大的好处是:你永远能复现出完全一样的环境。不管是一周后还是一年后,只要Pipfile.lock还在,就能装出完全相同的依赖。
poetry的poetry.lock——更现代的依赖管理
poetry是近几年比较火的工具。它的poetry.lock和pipenv的思路类似,但做得更精细。
# poetry.lock 片段
[[package]]
name = "requests"
version = "2.28.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7, <4"
[package.dependencies]
urllib3 = ">=1.21.1,<1.27"
certifi = ">=2017.4.17"
[package.files]
type = "url"
url = "https://files.pythonhosted.org/packages/.../requests-2.28.1-py3-none-any.whl"
hash = "sha256:xxx..."
poetry的锁文件有几个亮点:
- 解析速度快:它用SAT求解器来解析依赖,比pipenv的解析快不少
- 支持多平台:同一个锁文件可以在Linux、macOS、Windows上通用
- 依赖分组清晰:main、dev、test等分组一目了然
我在一个微服务项目里用过poetry,几十个服务各自有独立的poetry.lock。每次CI构建时,直接poetry install --no-dev,干净利落。再也没出现过“我本地能跑,服务器跑不了”的情况。
锁文件为什么能解决冲突?
这个问题其实可以拆成三点来看:
- 版本确定性:锁文件把每个依赖的精确版本固定下来。A包依赖B包1.0,C包也依赖B包1.0,那就不会出现A要1.0、C要2.0的冲突。
- 依赖树透明:锁文件记录了完整的依赖关系图。你能清楚地看到每个包为什么被安装,是谁依赖了它。
- 可复现性:只要锁文件不变,任何人在任何时间、任何机器上安装,得到的依赖树都是一模一样的。
.lock文件加到.gitignore里,理由是“文件太大”。结果团队里每个人装的依赖都不一样,调试起来鸡飞狗跳。
三种锁文件的对比
| 特性 | requirements.txt (pip freeze) | Pipfile.lock (pipenv) | poetry.lock (poetry) |
|---|---|---|---|
| 记录子依赖 | 否(只列包名) | 是 | 是 |
| 哈希校验 | 否 | 是 | 是 |
| 跨平台支持 | 差 | 一般 | 好 |
| 解析速度 | 快(但功能弱) | 慢 | 快 |
| 依赖分组 | 需手动维护 | 支持 | 支持 |
锁文件的工作原理(SVG流程图)
下面这张图展示了锁文件在依赖解析和安装过程中的核心作用:
避坑指南
最后分享几个我踩过的坑:
- 别手动改锁文件:锁文件是工具自动生成的,手动修改很容易导致格式错误或依赖不一致。我曾经手贱改了一个版本号,结果整个依赖树都乱了。
- 定期更新锁文件:锁文件不是一成不变的。每隔一段时间,用
pipenv update或poetry update更新一下,把安全补丁打上。 - 锁文件要提交到Git:这个前面说过了,但值得再强调一遍。没有锁文件的项目,就像没有地图的旅行。
- CI/CD中验证锁文件:在CI流水线里加一步检查,确保
poetry.lock和pyproject.toml是同步的。不然有人改了声明文件忘了更新锁文件,又是个坑。
锁文件这东西,说白了就是给依赖管理上了一道保险。虽然多了一个文件要维护,但换来的是环境的稳定和团队的安心。我个人觉得,这笔买卖很划算。