我为什么不喜欢 black
/ / / 阅读数:12518前言
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 个意见:
- 格式化后
HOME_REC_BANNED_TAG_NAME
行尾添加了逗号,我觉得这个逗号多余。我日常开发中都是按需添加逗号,不会多加。 - 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 一个版本,去掉那些我不认可的规则,应用到个人和团队的项目中~
所以说还是 Owner 决定一切。BTW,我是比较支持 Black 的,统一减少了很多不必要的麻烦,但是在旧项目上 Black 化是一件比较难以推进的事,目前在公司里,只能渐进式来做。
对于新项目,直接把 Black 加到 CI 中强制就可以了。