依赖可视化:用工具生成依赖树,发现冗余

依赖这东西,说白了就是一层套一层的。你装了个 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 会高亮显示版本冲突和缺失依赖,帮你提前发现隐患。

如何解读依赖树?

依赖树不是用来「看」的,是用来「分析」的。我给你一个我自己的分析框架:

  1. 识别根节点——哪些库是你直接依赖的?这些是安全的,不能随便删。
  2. 查找共享依赖——比如 urllib3 被 requests 和 pandas 同时依赖?嗯,这个不能动。
  3. 标记独有依赖——如果某个库只有一个上游依赖它,且这个上游可以被替换,那这个底层库就是冗余候选。
  4. 对比版本范围——注意看 [required: >=x.y] 和 [installed: x.y] 的差异,避免升级后出现兼容问题。

我的经验: 冗余往往藏在「独有依赖」里。比如你只用了 Flask 的一个小功能,但它带了一整套 Jinja2 模板引擎。如果你根本不用模板,完全可以把 Flask 换成更轻量的替代品。

从依赖树到冗余识别

有了依赖树之后,怎么判断哪些是冗余?我一般用「排除法」:

  • 直接未使用——你在代码里 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 路径被触发,再删。

本章小结

依赖可视化不是终点,它是你瘦身的第一步。记住这几个关键动作:

  1. 用 pipdeptree 生成全量依赖树
  2. 逐层分析,找出「独有依赖」和「功能子集」
  3. 对比版本范围,确认是否有兼容隐患
  4. 用测试覆盖验证删除安全性
  5. 考虑替换整个上游依赖来「一锅端」

说白了,工具只是帮你把黑盒变成白盒。真正动手做决策的,还是你自己。

一句话总结: 依赖树是你的「可视化雷达」,用好它,你就能精准定位冗余,而不是靠猜。