前言

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的处理是按照isort Multi 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