依赖可视化:用工具生成依赖树,发现冗余
依赖这东西,说白了就是一层套一层的。你装了个 requests,它可能又带了 urllib3、certifi、chardet……这些你都知道吗?
我刚开始做项目瘦身的时候,也犯过懒。觉得装了就用呗,管它带了啥。直到有一次,我把一个简单的 API 服务打包成 Docker 镜像,好家伙,直接 1.2GB。我盯着那个镜像大小,愣了半天。
后来我仔细一查,发现里面塞了一堆根本用不到的依赖。比如我们项目只用到了 pandas 的 read_csv 功能,但它把整个 NumPy、SciPy 的计算栈全拖进来了。这就是典型的「买一送十」。
所以,今天我们来聊聊怎么把这些「隐形乘客」揪出来。
为什么需要可视化?
依赖关系不是线性的,它是棵树。你以为是这样的:
A → B → C
但实际可能是这样的:
A → B → D → E → F
A → C → D → E → F
A → D → E → F
多个上游库都依赖同一个底层库,这种情况太常见了。你看 requirements.txt 只能看到第一层,底下藏着什么,完全是个黑盒。
我习惯先可视化再动手。为什么?因为只有看到了全貌,你才知道从哪里切下去最安全。
工具选择:pydeps 与 pipdeptree
Python 生态里有两个主流工具:pydeps 和 pipdeptree。我个人更常用后者,因为它输出更清晰,而且能直接帮你找到「孤儿依赖」。
| 工具 | 特点 | 我的用法 |
|---|---|---|
| pipdeptree | 文本输出、支持树形结构、能显示冲突 | 快速扫描整个项目的依赖树 |
| pydeps | 图表输出、支持 SVG/PNG、能显示依赖强度 | 做汇报或写文档时用,视觉上更直观 |
安装很简单:
pip install pipdeptree pydeps
实战:用 pipdeptree 生成依赖树
假设你有一个项目,requirements.txt 里写了三个库:
flask==2.3.0
pandas==2.0.0
requests==2.31.0
运行以下命令:
pipdeptree -p flask-pandas-requests
然后你会看到这样的输出:
flask==2.3.0
- click [required: >=8.0, installed: 8.1.3]
- itsdangerous [required: >=2.0, installed: 2.1.2]
- Jinja2 [required: >=3.0, installed: 3.1.2]
- MarkupSafe [required: >=2.0, installed: 2.1.3]
- Werkzeug [required: >=2.3.0, installed: 2.3.4]
pandas==2.0.0
- numpy [required: >=1.23.2, installed: 1.25.0]
- python-dateutil [required: >=2.8.2, installed: 2.8.2]
- six [required: >=1.5, installed: 1.16.0]
- pytz [required: >=2020.1, installed: 2023.3]
requests==2.31.0
- certifi [required: >=2017.4.17, installed: 2023.5.7]
- chardet [required: >=3.0.2, <6.0.0, installed: 5.1.0]
- idna [required: >=2.5, <4.0.0, installed: 3.4]
- urllib3 [required: >=1.21.1, <3.0.0, installed: 2.0.3]
你看到了什么?一个清晰的层级结构。每个库的「子孙」一目了然。
--warn 参数,pipdeptree 会高亮显示版本冲突和缺失依赖,帮你提前发现隐患。
如何解读依赖树?
依赖树不是用来「看」的,是用来「分析」的。我给你一个我自己的分析框架:
- 识别根节点——哪些库是你直接依赖的?这些是安全的,不能随便删。
- 查找共享依赖——比如 urllib3 被 requests 和 pandas 同时依赖?嗯,这个不能动。
- 标记独有依赖——如果某个库只有一个上游依赖它,且这个上游可以被替换,那这个底层库就是冗余候选。
- 对比版本范围——注意看
[required: >=x.y]和[installed: x.y]的差异,避免升级后出现兼容问题。
从依赖树到冗余识别
有了依赖树之后,怎么判断哪些是冗余?我一般用「排除法」:
- 直接未使用——你在代码里 import 都没 import 过的库,直接删。
- 间接未使用——一个库的所有功能都没被调用,但这个库却因为另一个库被拉进来了。这种情况需要评估是否能替换上游依赖。
- 功能子集——比如你只用了 pandas 的 DataFrame,却装了完整版。可以试试用 polars 或 pyarrow 替代。
- 版本过旧——老版本往往携带更多兼容性代码,升级到新版本有时能自动瘦身。
举个例子,我在一个项目里发现:
matplotlib==3.7.0
- cycler [required: >=0.10, installed: 0.11.0]
- fonttools [required: >=4.22.0, installed: 4.40.0]
- kiwisolver [required: >=1.0.1, installed: 1.4.4]
- numpy [required: >=1.20, installed: 1.25.0]
- packaging [required: >=20.0, installed: 23.1]
- pillow [required: >=6.2.0, installed: 9.5.0]
- pyparsing [required: >=2.3.1, installed: 3.0.9]
- python-dateutil [required: >=2.7, installed: 2.8.2]
我们项目里只用了 matplotlib.pyplot.plot() 画了一张图。你猜我们实际需要哪些依赖?其实只需要 matplotlib 本身和 numpy。剩下的像 fonttools、pillow、cycler,全都没用到。
最后我们直接把 matplotlib 换成了 plotly,安装体积从 80MB 降到 15MB。这是我做过的「性价比最高」的一次瘦身。
避坑指南:我曾经踩过的坑
我曾经在一个项目里看到依赖树显示 requests 依赖了 chardet,我觉得没用,就把它从 requirements.txt 里删了。结果运行时直接报错——requests 在编译时会自动检测并加载 chardet,我删了它就变成了 ImportError。
嗯,这里要注意:依赖树显示的是「安装时依赖」,不一定是「运行时依赖」。有些库会做动态导入,依赖树是看不出来的。
我的解决办法是:删之前先在测试环境跑一周,或者用 coverage.py 做一次全量代码覆盖率测试。确认没有 import 路径被触发,再删。
本章小结
依赖可视化不是终点,它是你瘦身的第一步。记住这几个关键动作:
- 用 pipdeptree 生成全量依赖树
- 逐层分析,找出「独有依赖」和「功能子集」
- 对比版本范围,确认是否有兼容隐患
- 用测试覆盖验证删除安全性
- 考虑替换整个上游依赖来「一锅端」
说白了,工具只是帮你把黑盒变成白盒。真正动手做决策的,还是你自己。