目录
告别 Flake8 和 PyLint:使用 Ruff 进行更快的代码检查
Flake8 和 PyLint 是常用且非常有用的代码检查工具:它们可以帮助你发现代码中的潜在错误和其他问题,即所谓的“lints”。但它们也可能很慢。即使它们在你的电脑上运行得很快,在 CI 系统(如 GitHub Actions、GitLab 等)中仍然可能很慢。
幸运的是,现在有一个新的代码检查工具 Ruff,它的速度要快得多。而且它支持许多与 Flake8 相同的检查,包括许多 Flake8 插件的检查。
在本文中,我们将了解为什么 Ruff 的额外检查速度如此有用,尤其是在与替代工具相比时。具体来说,我们将讨论:
- 一个不在标准 Flake8 中的有用检查。
- 与 Ruff 实现的速度比较(预览:它快得多!)。
- 为什么在你的电脑上看似很快的检查在 CI 中可能仍然很慢。
- Ruff 的最坏情况(预览:它快得离谱)。
- 为什么 Ruff 可能还不适合你。
- 通过更改
tox
配置来加速代码检查的额外技巧。
为什么 Flake8 不够用:一个例子
Flake8 本身是一个很好的工具,用于捕捉常见的基本错误。例如:
# example1.py
def f(myvar):
return myva * 2
$ flake8 example1.py
example1.py:4:12: F821 undefined name 'myva'
然而,它并不能捕捉所有问题。考虑以下程序:
# example2.py
def make_three_adders():
result = []
for i in [10, 20, 30]:
def add(x):
return x + i
result.append(add)
return result
for adder in make_three_adders():
print(adder(7))
你认为这个程序会输出什么?我们可能会期望它输出 17、27 和 37。但实际上:
$ python example2.py
37
37
37
我们创建的函数并没有捕获 for
循环中变量的当前值,而是使用了最后的值。
这是一个非常适合代码检查工具的场景,但默认情况下 Flake8 不会捕捉到这个问题:
$ flake8 example2.py
$
此时你有三种选择。首先,Flake8 有一个名为 Bugbear 的插件,它添加了许多额外的检查,包括可以识别这个错误的检查:
$ pip install flake8-bugbear
$ flake8 example2.py
example2.py:6:24: B023 Function definition does not bind loop variable 'i'.
其次,PyLint 也可以识别这个问题:
$ pylint example2.py
...
example2.py:6:23: W0640: Cell variable i defined in loop (cell-var-from-loop)
...
最后,你可以使用 Ruff 代码检查工具。
鉴于我在实际项目中见过这个问题导致的错误,我认为至少应该使用这些代码检查工具之一来检查它。
对三种代码检查工具进行基准测试
为了了解这三种代码检查工具在速度上的差异,我测量了在一个包含 12 万行 Python 代码的真实项目上运行这个特定检查所需的时间。我使用了 Python 3.11,在我的 i7-12700K 机器上运行,关闭了超线程和涡轮加速。并且我只运行了这个单一的检查:
$ time pylint --disable=all --enable=cell-var-from-loop src
$ time flake8 --select B023 src
$ time ruff --select B023 src
以下是实际时间和 CPU 使用情况的总结:
工具 | 实际时间(秒) | CPU 时间(秒) |
---|---|---|
Flake8 6.0.0 + bugbear 23.3.23 | 1.7 | 13.1 |
PyLint 2.17.3 | 14.0 | 14.0 |
Ruff 0.0.263 | 0.2 | 1.0 |
注意到 Ruff 和 Flake8 的 CPU 时间都比实际时间高:这意味着它们利用了多核的优势。
现在,很明显 Ruff 在 CPU 使用上快得多,但实际时间上 Flake8 和 Ruff 的差异并不大,因为 Flake8 可以利用我电脑的所有核心。那么,切换到 Ruff 真的值得吗?
为什么 CI 比我的电脑慢得多(可能也比你的慢)
不幸的是,当在 CI 中运行这个检查时,Flake8+Bugbear 版本的实际时间会比在我的电脑上长得多。这有两个原因:
- 更快的核心:我的电脑上的大多数核心是“性能”核心,可能比大多数 CI 系统使用的云虚拟机中的核心快两倍。
- 更多的核心:关闭超线程后,我的电脑有 12 个核心可用,假设我没有将它们用于其他任务。而 CI 中使用的云虚拟机通常只有 2 个 vCPU,而“vCPU”是超线程的委婉说法。也就是说,CPU 假装它是 2 个核心,但实际上是一个核心在共享。如果你幸运的话,可能会获得一点并行性,但远不及两个普通核心的性能。
本质上,我们必须假设 CI 运行器几乎没有并行性,并且其 CPU 相当慢。如果上表显示代码检查工具在我的电脑上花费了 14 秒的 CPU 时间,我们可以初步猜测它在 CI 中会花费 30 秒的实际时间。
你的个人电脑可能没有我的快,但即使是较慢的个人电脑,也可能比 2 vCPU 的云虚拟机快得多。
切换到 Ruff:一个真实的例子
考虑到 CI 运行器的速度较慢,Ruff 在 CPU 时间上的大幅减少变得更加有意义。最近,我将一个项目切换到 Ruff;之前它使用的是 Flake8 加上 PyLint 的 cell-var-from-loop
检查。基本上它运行了以下命令:
flake8 src
pylint pylint --disable=all --enable=cell-var-from-loop src
代码检查工具在 CircleCI 的“Medium”运行器上运行,该运行器有 2 个 vCPU。以下是时间对比:
运行时间 | |
---|---|
原始 | 100 秒 |
Ruff | 43 秒 |
Ruff + tox.ini 调整 | 19 秒 |
这 19 秒基本上都是开销:每次作业运行时,都需要检出代码、设置虚拟环境、安装 tox
和 ruff
等。运行 ruff
的速度如此之快,以至于它几乎不会影响实际时间。
额外加速:更改 tox
配置以进行代码检查
那么,那个节省了 20 秒的 tox.ini
调整是什么呢?默认情况下,tox 在创建新环境时会安装包,在这种情况下是为了进行代码检查。这可能会导致下载依赖项,并且写入大量小文件也需要一些时间。
但对于代码检查,我们根本不需要安装代码。因此,我们可以使用 tox 的 skip_install
选项来跳过安装包代码,从而节省一点时间。
Ruff 会变得多慢?
请注意,除了相对较快的 Flake8 基础设置外,我们只配置了一个额外的检查。在理想情况下,我们希望添加更多的检查;Ruff 默认禁用了许多检查(是否有用取决于你的情况)。
那么,如果我们启用 Ruff 的所有检查,它会花费多少时间呢?实际上,许多检查并不一定是每个人都想启用的,但它仍然为我们提供了一个速度上限:
$ time ruff --select=ALL src/
...
Found 55840 errors.
[*] 17570 potentially fixable with the --fix option.
real 0m0.566s
user 0m1.418s
sys 0m0.172s
即使我们启用了 Ruff 提供的所有检查,它的运行速度仍然比 Flake8 快 10 倍,比 PyLint 更快。
为什么 Ruff 可能还不适合你
Ruff 实现了一长串代码检查规则,直接复制了 Flake8、Flake8 插件、PyLint 和其他工具的规则。PyLint 的兼容性仍处于早期阶段,许多检查缺失;另一方面,使用 PyLint 非常困难且缓慢,以至于大多数检查可能默认都是禁用的。尽管如此,它的进展非常迅速。
对于新项目,我会直接开始使用 Ruff;对于现有项目,我强烈建议在你开始对 CI 中的代码检查速度感到恼火时(甚至在你的电脑上)尝试它。