前言

Black 是一个代码格式化工具,项目上个月刚迁移到 Python 组织下,意味着这是一个社区认可的项目。项目诞生不久我就点了 Star,到现在已经 1w+ star 了,但是我一直没写文章推荐过它,因为我并不完全认可它。

了解我的人都知道我是非常愿意接受和尝试新鲜事物的,凡是有益于社区的发展我都会支持。好像到现在,除了类型注解我不喜欢以外,其他的我都觉得 Ok,包含非常有争议的海象运算符 (PEP 572)。

今天闲来无事给大家聊聊这件事,本文不是纯技术文章,更多的是我的个人观点,请按需阅读。

为什么不喜欢

作者给 black 的定义是「The Uncompromising Code Formatter」,也就是「不妥协的代码格式化程序」。什么意思呢,一句话:你要听它的,由 black 按照它的审美帮助你处理代码格式问题。black 的格式化规则是 PEP8 的超集,也就是除了处理成符合 PEP8 规范要求的代码,也有 black 的一些规则。

说到这里让我想到了 Golang 的 gofmt 工具: Golang 的开发团队制定了统一的官方代码风格,使用官方的 gofmt 帮助开发者格式化他们的代码到统一的风格。虽然这样降低了自由度,但是极大的节省了开发者对于代码风格的纠结,争论等等,现在写 Go 代码时我会在编辑器配置保存时自动格式化,我非常喜欢这么用。

当你对 Python 语言语法和 PEP 8 非常熟悉,手写的代码基本符合 PEP8 的要求,那么这个时候你对代码格式化的感受应该和我一样。我现在如果写完代码运行 flake8 有 1-2 个错误 (我写 Python 代码不配置 autopep8 等工具),我会视错误类型考虑直接用 autopep8 这样的工具直接解决问题:因为没必要手改,看错误就知道什么问题了。这个时候就要充分利用代码格式化工具的效率了。

所以我不是不喜欢代码格式化,而是非常喜欢。所以不喜欢的原因是black 里面的规则。如果你用过 black,使用black --help可以发现它的参数非常少,几乎没有配置参数,这更印证了不妥协:按我的来就可以了。

我举 2 个规则例子。

单引号 or 双引号

默认 black 要求你必须用双引号把字符串包起来,而不是单引号。如果你看过我写的代码,会发现我 99% 用单引号,如果没有必要我是从来不会用双引号的。其实在 black 早期版本 (18.6b0 之前),是没有 - S 参数 (--skip-string-normalization) 选择要不要把字符串标准化。作者非常坚持的认为首选应该是双引号,但事实上非常多开发者都是用单引号的,或者单双不敏感的,所以有一些开发者提了个 Issue(延伸阅读链接 1),这个讨论很长,有很多开发者参与,有兴趣的可以看看。从结果来看,虽然加了选项 (目前唯一个可由选项值决定格式化效果的),作者是不情愿的妥协了,但是通过整个对话中可以看到作者对于自己观点的固执和...,想了一会不知道用什么词,就说洁癖吧。对于这一点我是非常理解的,聪明的、有天赋的人大多具备这样的特质:不妥协,而且这属于开发者自己的风格,Owner 可以决定项目的一切。

在写这篇文章时,我翻了下最近几个月核心开发者对 CPython 提交的代码,大部分都是用单引号,另外有些开发者 (如 vstinner,ncoghlan,asvetlov,benjaminp 等) 对单引号双引号不敏感,混用。其实 Black 作者 ambv 给 CPython 提交的代码中也是混用的。所以推荐标准化字符串引号用双引号这个规则我不能理解和接受。我接受并且会改正一切现在被认为是正确的用法,哪怕过去是反模式 (Anti-pattern) 的。我举个 PEP8 W503/W504 的例子:

# Line break occurred before a binary operator (W503)

## 反模式👇
income = (gross_wages
          + taxable_interest)

## 最佳实践
income = (gross_wages +
          taxable_interest)

过去我写的代码是👆这种最佳实践风格的:运算符放在上一行结尾。但是后来加了 W504 错误:

# Line break occurred after a binary operator (W504)
## 反模式👇
income = (gross_wages +
          taxable_interest)

## 最佳实践
income = (gross_wages
          + taxable_interest)

是不是有点懵,这 2 种错误是不是很让人抓狂?无论你写成那种风格都会抛另外一种风格错误,非常有趣。而且过去的最佳实践成为了反模式,过去的反模式成了最佳实践!

我学 PEP8 很早,由于这个 PEP 改动频率极低,很久我都没看了。结果 16 年让我重学一次 PEP 8。但是这种学习和接纳是必要的。

说回来这件事,按我的做人做事风格,我会接纳使用它的开发者的意见,一个人提的观点我可能不重视,但是如果有多人都提出意见那么我肯定会说服他们(至少要说服大部分),或者对这部分做出妥协。

import 的多行输出模式

在大型项目里面一个模块中从其他模块导入的内容是非常多的,black 对 import 的处理是按照 isortMulti line output Mode 3+ trailing comma 实现的 (isort 支持的模式很多,可以看延伸阅读链接 4 了解)。假如有这么一句:

from sansa.models.consts import (
    BLACKLIST_GROUP_ID, BANNED_GROUP_IDS, TAG_REC_POOL,
    STORY_TAG_HOME_REC_BANNED, HOME_REC_BANNED_TAG_NAME)

black 会直接格式化成:

from sansa.models.consts import (
    BLACKLIST_GROUP_ID,
    BANNED_GROUP_IDS,
    TAG_REC_POOL,
    STORY_TAG_HOME_REC_BANNED,
    HOME_REC_BANNED_TAG_NAME,
)

这部分可以看 Issue 127 (延伸阅读 5)。对我来说,我有 2 个意见:

  1. 格式化后 HOME_REC_BANNED_TAG_NAME 行尾添加了逗号,我觉得这个逗号多余。我日常开发中都是按需添加逗号,不会多加。
  2. black 强制限制了开发者 import 的格式化方案。我日常写代码,用的是 Multi line output Mode 6,也就是 Hanging Grid Grouped, No Trailing Comma。配置和效果类似这样:
❯ cat .isort.cfg
[settings]
line_length=79
multi_line_output=6
include_trailing_comma=False
force_grid_wrap=0
use_parentheses=True
❯ isort test.py
❯ cat test.py
from sansa.models.consts import (
    BANNED_GROUP_IDS, BLACKLIST_GROUP_ID, HOME_REC_BANNED_TAG_NAME,
    STORY_TAG_HOME_REC_BANNED, TAG_REC_POOL
)

也就是说本来我的代码写的风格没问题,但是经过 black 格式化之后,还要用 isort 再过一遍。black 给我选的 Mode 3 (Vertical Hanging Indent) 我不喜欢。如果你了解知名的 Python 开源项目,你会发现 Mode 6 风格的有很多:Django、Sentry、Requests、Pipenv、IPython、DRF、Pip、mypy。而 Mode 6 的只翻到了 Mode 3。emmm...

PS:其他模式的我没有列出来,另外对于模式 3 和模式 6 我没有完整确认,可能有些项目混用了多种模式,这毕竟是开发者相关的。

综上所述,我觉得没必要限制多行import的格式化效果。

不喜欢和不用

Black 作者是非常知名的 Python 核心开发,无论技术能力和社区贡献我都不可企及,我写这篇文章算是妄议。

有一点,不喜欢不等于不用,我过去已经在厂内一个 Top 3 大型项目中应用了 black,主要是处理一些遗留代码的格式化问题。

那我的性格,如果某一天 black 成为 flake8 这样的 Python 开发标配,我会选择 Fork 一个版本,去掉那些我不认可的规则,应用到个人和团队的项目中~

延伸阅读

  1. https://github.com/python/black/issues/118
  2. https://lintlyci.github.io/Flake8Rules/rules/W503.html
  3. https://lintlyci.github.io/Flake8Rules/rules/W504.html
  4. https://github.com/timothycrosley/isort#multi-line-output-modes
  5. https://github.com/python/black/issues/127