项目依赖分析:先摸清家底,再谈瘦身
做库瘦身的第一步是什么?不是直接删代码,也不是盲目换轻量替代品。你想想看,连项目里到底引了哪些库、它们之间什么关系都不清楚,怎么敢动手?我在早期一个数据可视化项目上就吃过这个亏——看着 requirements.txt 里只有 20 行,结果生产环境一部署,pip install 跑了三分钟。后来一查,好家伙,背后拖进来 80 多个间接依赖。
说白了,依赖分析就是给项目做个“体检”,看看哪些库是必需的,哪些是冗余的,哪些版本冲突了。这章我会带你熟悉两个趁手的工具:pip freeze 和 pipdeptree,以及几个常用辅助命令。
1. pip freeze:最基础的依赖快照
pip freeze 应该是每个 Python 开发者都用过的命令。它会把当前环境中所有已安装的包(包括间接依赖)以 包名==版本号 的格式列出来。
pip freeze > requirements.txt
嗯,这条命令简单到不能再简单。但我要提醒一句:千万别直接在全局环境里跑,否则你会把系统级包也带进去。个人习惯是在虚拟环境或项目隔离环境里执行。
我曾经帮一个同事排查问题,他拿全局环境的 freeze 清单部署到 CI,结果少了好几个底层依赖。为什么?因为全局环境里有某个库的 C 扩展包,而新环境没有。从那以后,我每次都在项目根目录下先激活虚拟环境,再执行 freeze。
输出格式长这样:
Flask==2.3.2
Jinja2==3.1.2
MarkupSafe==2.1.3
Werkzeug==2.3.3
click==8.1.3
...
你看,只列了包名和版本,没有层级关系。如果项目依赖了 Flask,Flask 依赖了 Werkzeug,这里全都平铺显示。你很难一眼看出谁是直接依赖、谁是间接依赖。所以,纯靠 pip freeze 做瘦身分析,不够用。
pip freeze --exclude-editable 过滤可编辑安装的包,但依然无法去重间接依赖。更可靠的做法结合下面的工具。
2. pipdeptree:看清依赖的“族谱”
pipdeptree 才是真正帮你理清依赖关系的利器。它把包之间的父子关系用树状图展示出来,你想跳过哪个间接依赖,一目了然。
先安装它:
pip install pipdeptree
然后运行:
pipdeptree
输出示例:
Flask==2.3.2
├── click==8.1.3 [required: >=8.0, installed: 8.1.3]
├── importlib-metadata==6.7.0 [required: >=3.6, installed: 6.7.0]
│ └── zipp==3.15.0 [required: >=3.6, installed: 3.15.0]
├── itsdangerous==2.1.2 [required: >=2.0, installed: 2.1.2]
├── Jinja2==3.1.2 [required: >=3.0, installed: 3.1.2]
│ └── MarkupSafe==2.1.3 [required: >=2.0, installed: 2.1.3]
└── Werkzeug==2.3.3 [required: >=2.3.0, installed: 2.3.3]
└── MarkupSafe==2.1.3 [required: >=2.0, installed: 2.1.3]
pipdeptree==2.10.0
setuptools==68.0.0
wheel==0.40.0
看到了吗?Flask 下面挂了 click、Jinja2、Werkzeug,而 Jinja2 又依赖 MarkupSafe。MarkupSafe 被两个父包同时引用,但树里会重复显示(安装一遍即可)。这种结构让我能快速判断:哪些包是“根”节点(直接依赖),哪些是“叶”节点(只被一个上层引用)。
我在一个 API 项目里遇到过这样的坑:requests 和 httpx 都依赖了 certifi,但 pipdeptree 显示它们版本要求冲突。最后我统一了这两个 HTTP 库的版本,才解决。没有树状视图,这种冲突很难定位。
常用参数速查
| 参数 | 用途 | 示例 |
|---|---|---|
--packages |
只显示指定包及其依赖树 | pipdeptree --packages Flask |
--reverse |
反向查看某个包被哪些上层包依赖 | pipdeptree --reverse --packages certifi |
--warn |
显示冲突警告(默认开启) | pipdeptree --warn silence |
--json |
输出 JSON 格式,方便脚本处理 | pipdeptree --json |
pipdeptree --reverse 可以快速找到“被多个上层依赖的公共包”,这些包往往是瘦身的突破口——如果干掉一个上层依赖,可能同时删掉多个间接依赖。
3. 其他辅助工具
除了上面两个主力选手,我还常搭配以下命令做深度分析:
pip check:检查当前环境有无版本冲突或缺失依赖。我每次升级项目之前都会跑一遍,避免“升级一时爽,修复火葬场”。pip-licenses:一个第三方工具,可以列出所有包及其许可证,方便合规审查。瘦身时顺便看看有没有不再维护的包。pip show [包名]:查看单个包的详细信息,包括依赖关系、安装路径。我经常用它来核实某个间接依赖的来源。
pip freeze 生成清单后直接删了 requirements.txt 里不认识的包,结果导致 CI 构建失败。后来才意识到,有些间接依赖虽然没直接写进代码里,但运行时会被底层库加载。所以,一定要先看 pipdeptree 的树状结构,分清主次。
4. 一张图看懂依赖分析流程
下面这张流程图概括了我们做依赖分析时的完整链路:
5. 实战:分析一个真实项目的依赖
假设你接手了一个 Flask+requests+celery 的项目。我会这样做:
- 进入虚拟环境:
source venv/bin/activate - 生成完整树状结构:
pipdeptree > deptree.txt - 找出所有顶层包:看树的第一层,手动整理或使用
pipdeptree --packages Flask requests celery只看你关心的。 - 检查冲突:
pip check,如果报错立刻记录。 - 反向查找公共依赖:比如
pipdeptree --reverse --packages six,如果 six 被 10 个包引用,那想干掉它很难;如果只有一个废弃包在引用,就可以考虑替换那个废弃包。 - 形成瘦身建议:哪些包只被一个不常用的库引用,且那个库有更轻量的代替品。
我记得有个项目里用了 Pillow 只是做图片格式转换,但全量安装带来了大量编解码模块。通过 pipdeptree 发现它没有被其他包依赖,最后我们用 pillow-simd 替代,体积缩小了 40%。
总结表格:工具对比
| 工具 | 输出形式 | 主要用途 | 适合场景 |
|---|---|---|---|
| pip freeze | 平面列表 | 快速导出版本锁定文件 | CI/CD 部署、环境复刻 |
| pipdeptree | 树状结构 | 分析依赖层级、冲突、冗余 | 瘦身调研、依赖清理 |
| pip check | 冲突报告 | 验证环境一致性 | 升级前后检查、代码评审 |
| pip show | 包详情 | 查单个包的依赖和位置 | 定位具体问题包 |
pipdeptree 并将结果保存到版本库。这样后续任何依赖变动都有迹可循,瘦身的时候能更快定位“谁拖累了你”。