本文介绍 Python 代码风格检查的实践,其中第3节渐进式介绍了4种方案,也可以直接看第4节“最佳实践”。
1. 项目需要统一的代码风格
代码风格类似文章的排版要求,一本杂志、书籍往往由多人合著,作者们需要达成共识用统一的排版风格。程序同样由多个程序员合作编写而成,甚至还会互相修改其他成员的代码,统一的风格犹为重要。
2. Python 项目的代码风格
Python 项目的代码风格,我认为需要分成两层:
- PEP 8,这是 Python 社区的共识,应该作为我们项目代码风格的底线。然而,PEP 8 的规定相当宽泛,符合 PEP 8的前提下,仍然可以写出风格迥异代码。因此,项目通常又会在 PEP 8的基础上制定
- 项目风格要求。在PEP 8基础上制定项目风格,实际上是基于一些的原因增加限制。比如规定字典元素太多时该如何换行。风格一,尽量在一行写入最多的元素,原因是这样能使代码更紧凑。
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4',
'key5': 'value5', 'key6': 'value6'}
风格二,一行一个元素,原因是将来增删改元素只会影响到一行,方便 diff review。
{
'key1': 'value1',
'key2': 'value2',
'key3': 'value3',
'key4': 'value4',
}
都有道理,需要项目成员达成共识,制定出项目的风格要求。
3. 代码风格检查
代码风格制定好了,如果没有相应检查机制,我相信制定好的代码风格并不会得到完整的执行。 如何检查 Python 代码风格是本文重点。我把代码风格检查机制按照自动化程度分成以下层次:
- 人工检查;
- 手动运行检查器;
- 自动运行检查器;
- 自动修正代码风格;
3.1. 人工检查
人工检查是最原始的,在 code review 的时候,看看是不是有代码风格问题。熟悉 PEP 8的 Python 程序员应该具备肉眼检查 PEP 8风格的能力,通常风格问题是很碍眼的,想看不出来都难。 然而人工检查存在着明显的问题:
- code review 还要把精力放在代码风格上;
- code review 阶段还要提一批风格相关的 issues;
- code review 阶段还得来回修改代码风格问题;
- 人毕竟不是机器,可能有遗漏。
缺点: 显然,人工检查存在效率和严谨性的问题,更何况有些项目并没有对每一个 commit 做 code review,人工检查得等到什么时候触碰到这些代码时才能进行。
3.2. 手动运行检查器
代码风格检查工作如此的机械化,显然适合让机器做,而不必浪费程序员的精力。
常用的检查器有:pep8 · PyPI、flake8、pylint。推荐 flake8,其结合 pep8 和其他一些风格检查,基本上忠实于 pep8,不会带来太多打扰。不推荐 pylint 是因为 pylint 的默认配置非常糟糕(不是 PEP 8,也不是哪个社区的共识),非开箱即用,得根据项目代码风格精心配置,否则相当打扰。不过 flake8 的可定制性较差,额外的项目风格无法检测,pylint 相对来说可定制性更高。
有了检查器,程序员可以在提交代码前运行检查,根检查器报告修改,直到完全通过。进阶一点可以用相应的编辑器插件,边写边检查。
缺点: 手动运行的问题是,程序员可能忘记运行了,或者新加入项目的程序员根本不知道有这个规则。
那么我们自然会想到自动运行检查器。
3.3. 自动运行检查器
自动运行检查器有这些方案:
这些方案中我推荐客户端 Git Hooks。 相对服务端 hooks,客户端 hooks 不需要服务端支持,适应性高;相对于 CI,搭建成本低,且在代码提交前就检查好风格,而不是到了集成阶段再来检查。
客户端 hooks 的原理非常简单,.git/hooks 目录下有很多 .sample 文件,是各个阶段 hook 的例子。
$ ls .git/hooks/
applypatch-msg.sample pre-applypatch.sample pre-receive.sample
commit-msg.sample pre-commit.sample prepare-commit-msg.sample
fsmonitor-watchman.sample pre-push.sample update.sample
post-update.sample pre-rebase.sample
这里我们需要 pre-commit hook,创建一个 pre-commit 文件,内容是 shell 脚本,运行检查器。如果检查不通过就会阻止 commit。通过这样的机制就能确保检查器运行且检查通过。
然而,客户端的 hooks 是在本地的,不在仓库里管理,也就是说 hooks 不能共享不能同步。为了解决这个问题,又有了 pre-commit 这个工具,只需要在仓库根目录放置一个 .pre-commit-config.yaml ,定义需要的 hooks(github 上很多开源的 hook 可以在这里直接引用,当然也包括 flake8),把这个文件提交到仓库。然后在客户端运行一次 pre-commit install --install-hooks
即可注册到 .git/hooks/pre-commit 里,后继 .pre-commit-config.yaml 更新也同步到各个客户端。用法详见第4节。
到目前为止,已经做到确保代码在提交到仓库之前是通过代码风格检查的。
缺点: 要说还有什么不足,那就是检查虽然是自动,但修正还得手动。那么有没有自动修正的方案?
3.4. 自动修正代码风格
Python 代码风格自动修正,目前最好的应该是 Black。Black 是 PSF(Python Software Foundation,Python 软件基金会)的项目。为什么要强调是 PSF 的项目?因为符合 PEP 8的写法可以有很多种,而修正工具只能修正成其中一种。 那么哪一种写法才大家都喜欢的?这就需要项目成员达成共识,也就是第2节提到的项目的风格要求。而 Black 作为 PSF 的项目,一定程度上代表了 Python 社区的共识。 那么,我们没必要再花精力制定项目代码风格,直接交给 Black 就好了。
Black 在文档详细介绍了其代码风格及相应的原因,比如为什么默认一行的长度限制是88字符(可以配置),比 PEP 8标准增加10%,有兴趣可以看一下。
同样,把 Black 也加到 pre-commit hook 里,那么在提交代码的时候就会自动修正代码风格。再加一道风格检查,把不能自动修正的提示出来,让提交者手动修正。
这样就形成了本文的最佳实践,具体做法见下一节。
4. 最佳实践
在仓库根目录放置以下3个文件,并提交。执行一次 make init
即完成所有配置工作。
实现的功能有:在 commit 的时候,
- 自动重排 import 顺序,
- 修正代码风格,
- 检查代码风格,
- 修正其他文件格式问题。
.flake8
[flake8]
max-line-length = 80
select = C,E,F,W,B,B950
ignore = E203,E501,W503
.pre-commit-config.yaml:
repos:
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.8.0
hooks:
- id: reorder-python-imports
name: Reorder Python imports (src, tests)
- repo: https://github.com/python/black
rev: stable
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: check-byte-order-marker
- id: trailing-whitespace
- id: end-of-file-fixer
Makefile:
init:
python3 -m venv .venv
.venv/bin/pip install pre-commit
.venv/bin/pre-commit install --install-hooks
clean:
rm -rf .venv
.PHONY: init clean
该方案受 Flask 的 contributing guidelines 启发。