拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Black:用自动化格式化终结Python代码风格之争

Black:用自动化格式化终结Python代码风格之争

很多刚接触 Python 的朋友都有过这种体验:代码写着写着就乱了,缩进时好时坏,引号有时候单有时候双,一行代码长得拖出屏幕好几屏。代码能跑,但自己看着都别扭,更别说交给别人 review。最开始我都是手动一点一点整理,后来才发现这类重复劳动从一开始就应该交给工具。项目不论大小,代码规范这东西就得靠自动化来解决,手动纠错永远不是长久之计。

今天聊的 Black 就是 Python 生态里目前最主流的自动格式化工具。它和 pylint、flake8 这类只负责“挑毛病”的静态检查工具不一样,Black 是直接帮你改代码的,格式化规则内置,几乎不需要配置。它可以做到:统一缩进、统一引号、统一换行、统一尾随逗号的处理规则,让整个项目的代码风格高度一致。本文适合刚开始做 Python 项目、或者正在维护老项目、又或者组建团队准备定规范的朋友参考,直接照着配置就能用,不用再纠结“代码风格”这个永远吵不完的话题。

1. 为什么需要自动格式化,以及 Black 的定位

1.1 手动排版代码到底浪费了多少时间

写 Python 代码,缩进就是语法的一部分。手动的坏处不是你“不会”放缩进,而是你会在不同文件、不同时间、不同心情下做出不同风格的排版。比如有的地方你喜欢用单引号,有的地方用双引号,有的地方在函数调用时把参数拆成多行,有的地方又随手写在一行。这些都是合法的 Python 代码,风格却能差出十万八千里。等到多人协作时,每个人带来的“个人风格”混在一起,代码库就像拼贴画一样零碎混乱。说浪费时间一点也不夸张:code review 的时候至少三分之一的评论是在说“这里格式可以统一下”,而不是在聊业务逻辑。

你可能觉得这事很小,但研究表明,人在阅读代码时有相当比例的认知负担花在“解码格式不一致”这件事上。同一个项目里换个文件风格就变,脑子得不停切换模式,阅读效率自然上不去。反之,格式统一的代码能让人快速跳过外观,直接进入逻辑。这也是为什么 Black 这类工具能在近几年快速普及,它把“风格争论”从人之间转移到了工具上。

1.2 Black 与其他工具的分工:不冲突,反而是互补

Python 社区有个铁三角组合我一直在用:Black 负责格式化,isort 负责 import 排序,flake8 负责代码规范检查。如果项目再严格一点,还有 mypy 做静态类型检查,pre-commit 负责在提交前自动跑一遍所有检查。这四五个工具各司其职,Black 干的事最“粗暴”但也最基础:拿到代码,按规则重写一遍。它不会告诉你哪里写得不好,它直接帮你改。

很多人会担心“自动改代码会不会破坏什么逻辑”,这个担心是多余的。Black 是一个完全基于语法分析的格式化工具,它不改变程序语义,只改变代码的书写形式。也就是说,格式化前后的代码经过解释器执行,结果完全一致。这一点也可以从设计哲学上印证:Black 的官方宣传语是“Uncompromising”(毫不妥协),意思是它不会给你一堆配置项来商量风格,所有规则早就定好了,你只需要决定“用”还是“不用”。头几次用,你会觉得它很霸道,习惯以后就会觉得真香,因为不用再在配置上花精力。

1.3 Black 的设计哲学为什么是“零配置”

刚才提到“零配置”,这其实是个非常刻意的选择。在 Black 之前,大家更熟悉的工具是 autopep8 和 yapf。autopep8 只修复 pep8 违规,很多风格问题它不管;yapf 给了很多配置项,理论上可以微调到极致,但问题是每个项目都得维护一份长得要命的配置,团队里还得讨论每个参数的意义。Black 走的是另一条路:直接设定一套风格标准,省去讨论配置的过程,所有人都默认遵守这一套规则。

这在工程实践中是一个很聪明的取舍。你想想,代码格式化的终极目标是什么?是让代码风格统一、让工具链稳定、减少人为决策。如果格式化工具本身还需要大量配置,那等于问题的下半部分还没有解决。Black 的“零配置”让它在 CI(持续集成)中非常好用,任何人拉下代码,跑一条命令,结果都一样,不存在“我本地配置和你不一样”导致的差异。这让我个人在团队推广 Black 时几乎没有遇到阻力,因为完全不需要讨论。

2. 安装 Black 并跑通第一遍格式化

2.1 安装和版本选择

Black 的安装非常基础,有 pip 就能装。我建议在虚拟环境里安装,避免污染全局 Python 环境。简单起见,如果你的项目还没建虚拟环境,可以先用下面的命令装一个试试:

pip install black

Black 的版本迭代速度不算慢,每个大版本之间格式规则会有微调。比如 22.x 时代对某些括号换行策略做过调整,23.x 改过空行处理逻辑。团队内部最好锁定一个版本,免得换一台机器结果格式变了。用 requirements.txt 或 pyproject.toml 锁定即可。

想要确认版本,可以运行:

black --version

我建议至少使用 23.x 以上版本,稳定性和对新语法(比如 match 语句、类型参数)的支持更完善。如果项目还在用老版本的 Python,也要注意 Black 对不同 Python 版本的语法兼容情况。只格式化代码的话,Black 自己运行的环境并不一定和项目运行的环境一致,它只是做文本级的语法解析,所以大部分情况下不需要匹配。

2.2 快速上手:格式化单个文件

Black 的用法非常简单。进入项目根目录,直接:

black my_script.py

它会在终端输出哪些文件被格式化、哪些文件没有变化。默认情况下 Black 会直接改写原文件,如果你还不确定效果,可以先加--check参数只检查不修改:

black --check my_script.py

如果想看改动前后的差异,用--diff:

black --diff my_script.py

这俩参数是刚开始尝试 Black 时的最佳组合。既不会改坏代码,又能让你直观看到它究竟会怎么重排你的风格。

2.3 一次格式化整个项目的正确姿势

项目大了,别一个文件一个文件地去格式化。直接对目录操作:

black .

这个命令会递归扫描当前目录及其子目录下所有*.py文件(默认不进入隐藏目录和虚拟环境目录)。这是不是意味着可以无脑用?也不是。如果项目里有些文件是从其他地方生成的,比如 protobuf 生成的 pb2.py 文件,你就不希望 Black 去动它。这种时候可以用--exclude参数:

black --exclude "/(generated|pb2)\/" .

或者更推荐的做法是直接在pyproject.toml里配置:

[tool.black] line-length = 88 target-version = ['py39'] extend-exclude = ''' /(generated|pb2)/ '''

这里extend-exclude用的是 gitignore 风格的路径匹配。项目根目录下建立pyproject.toml以后,每次跑black .都会自动读取这份配置,不用再反复敲参数。

2.4 格式化结果怎么验收

有一个很容易忽略的点:格式化完别忘了跑一遍测试。虽然 Black 不改变语义,但格式变化可能让某些依赖“字符串行号”的测试误报。更稳妥的做法是格式化结束后,跑一下现有测试套件,确认绿色再提交。不要觉得这一步多余,尤其是在大型重构或多文件同时格式化的场景下,测试是最后的防线。

3. Black 的核心格式化规则到底改了什么

3.1 数字、引号、空行和缩进的基础统一

Black 最基础的规则包括:

  • 字符串引号统一:优先使用双引号。它不是把已经在用双引号的改成单引号,而是把含单引号但不需要转义的字符串统一转为双引号。字符串里本身有双引号时,则保留单引号,也就是用最省事的方式避免转义。
  • 行尾统一处理:默认保留一个空行在文件末尾,这一点很多入门者会漏掉。
  • 缩进:统一为 4 个空格,永远不用 tab。
  • 空行:顶层函数和类定义之间统一保留两个空行,类内部方法之间保留一个空行。

你可能觉得这些都是小事,但积少成多就是代码整洁度的大区别。比如有的同事喜欢在函数定义之间用一个空行,有的用两个,还有的干脆没有。Black 格式化之后,这些全部标准一致。

最特别的一点是它会把一个容易忽略的事做到极致:函数调用或定义时,如果参数列表需要换行,Black 会保持一种“魔法逗号”风格。什么意思?就是如果你的最后一个参数后面有逗号,Black 会强制把每个参数独立一行并保持“悬挂缩进”,因为这样可以减少后续增删参数的 diff 行数。

对比一下,格式化前:

result = very_long_function_name( first_argument=1, second_argument=2, third_argument=3)

Black 格式化后:

result = very_long_function_name( first_argument=1, second_argument=2, third_argument=3, )

这种写法在代码评审里特别受欢迎,因为下一次加第四个参数时,git diff 只显示新增的一行,原来的三行不用动。

3.2 每行 88 字符的限制是怎么来的

Black 默认的行长度是 88 字符,不是 pep8 标准的 79。这个选择有其现实原因:79 是早期终端时代的产物,现在的屏幕宽了,而且 88 这个数字比 79 多出一点空间,能减少不必要的换行,同时保证在 GitHub 的 diff 界面上仍然友好显示。Black 的理念是“宁可改到 88,也不让开发者反复权衡”。如果你希望项目整体用 100 甚至 120,可以在配置里改line-length,但建议团队统一,而不是各改各的。

这个 88 长的选择也有另一个考量:如果代码太长,Black 会把表达式折叠成多行;如果只有几个字符超出,它可能会选择一种更紧凑的格式。它内置了一套复杂的括号分割算法,不是简单地在 88 字符处硬截断。比如二元运算符两侧的表达式较长时,它会把整个表达式拆成多行,并且把运算符放在行首,方便阅读连续的运算逻辑。方法链调用过长时,它会按“点”分割,让每个方法调用单独一行。

3.3 那些容易引起争议的强制风格

Black 有几个特色规则,第一次用会让人有点不习惯,但用久了你就知道它是故意的。

一个是把%格式化改为更一致的风格?不是,它不管逻辑层。但在语法风格层,它有一些强硬操作,比如:它会强制在=等赋值操作符两侧保留一个空格,并且会把空参数列表的括号内部清空。 isinstance 写法、if 语句中的布尔表达式,该拆行就拆行。

另外一个比较容易被误解的点:它会把一个多行 if 语句拆成不同风格的缩进。比如:

if ( some_condition_a and some_condition_b ): do_something()

这种写法更清楚地分隔了条件和逻辑块。当年很多老派 Python 开发者喜欢把and放在行尾,但 Black 坚持放在行首,理由是可以一眼看到逻辑连接符,方便阅读那一长串条件。从 diff 角度来看,行首逻辑符也更友好,因为调整条件的时候不容易“漏掉”行尾的and。

它甚至会对字典、列表、元组的尾随逗号做强迫症级别的处理。如果你的代码里有一个多行字典,最后一个键值对后面少了逗号,Black 会毫不犹豫地给你补上。这看起来是个小动作,实际上能避免一个经典的大坑:在多行结构里,如果你在某一行后面漏了逗号,下一行其实还是在同一行结构里,很容易出现莫名其妙的字符串拼接问题。Black 直接把这个问题扼杀在摇篮里。

3.4 代码注释和字符串会被怎么处理

Black 对注释的策略比较保守:它尽量不修改注释内容,只对注释位置做一些调整。比如一个注释本来顶格写在代码上面,Black 一般会保持原样。如果一段代码因为过长被拆行,那注释会跟着第一行或最后一行走。这不完美,Black 的官方说明也承认注释和字符串是格式化中最难兼顾的部分。字符串常量不会被重排,也不会被合并拆分,你原来的长字符串是什么样还是什么样。这一点要心里有数:Black 不是所有问题都替你解决,它只解决结构性排版问题。

4. 与 isort、flake8、pre-commit 的生态组合实操

4.1 import 排序交给 isort

Black 不负责整理 import 语句的顺序。import os和from collections import defaultdict谁在前谁在后,Black 不管。这时候需要 isort 出场。isort 和 Black 的配合在社区里已经非常成熟,唯一要注意的是两者的兼容配置。isort 的默认行为和 Black 有冲突的地方主要出在 import 换行的处理上。好在 isort 5.x 以后的版本内置了black的 profile,在配置里写:

[tool.isort] profile = "black"

这套配置会把 isort 的换行策略调到和 Black 一致,两者就不会出现“你改完他再改回”的尴尬情况。顺序上,我通常先跑 isort 再跑 Black,避免 import 排序的改动被 Black 重新排版产生多余 diff。

4.2 flake8 负责提醒最后那点事情

Black 和 isort 把排版搞定了,剩下的是 flake8 的活:检查未使用的 import、未定义的变量、行尾空白、复杂度过高、还有 pep8 中的部分规则。需要注意 flake8 对行长的检查默认是 79,和 Black 的 88 冲突。推荐装一个flake8-bugbear和pyproject-flake8,然后配置:

[tool.flake8] max-line-length = 88 extend-ignore = "E203,W503"

E203是切片空格相关的规则,和 Black 的切片空格策略不相容,必须忽略。W503是二元运算符换行位置规则,Black 坚持运算符在行首,因此也要关掉。这个配置是社区公认的“和 Black 兼容”的黄金组合。如果你用的 flake8 版本较老,注意extend-ignore字段不是所有版本都支持,记得升级。

4.3 pre-commit 自动化整个检查流程

如果每次提交代码都要手动跑一遍 Black 和 isort,总有忘记的时候。我的做法是接入 pre-commit。在项目根目录建一个.pre-commit-config.yaml:

repos: - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort name: isort (python) args: ["--profile", "black"] - repo: https://github.com/psf/black rev: 24.1.0 hooks: - id: black language_version: python3 - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8

然后运行一次pre-commit install,之后每次git commit时,pre-commit 会自动对暂存区里的 Python 文件依次执行这些工具。没通过就拒绝提交,绝不含糊。这里有一个我踩过的坑:如果项目同时用了pre-commit和pyproject.toml里的 Black 配置,需要确保pre-commit的缓存版本和本地安装的版本一致。否则可能出现一种奇怪情况:本地格式化通过,pre-commit 却失败。建议在 CI 里锁定版本,同时在本地也用同样的版本。

4.4 CI 里的校验要不要用--check

接入 CI 时,不要直接跑black .,因为这会改写代码然后整个提交。CI 里应该用black --check .和isort --check-only .,这样只检查不写入,让流水线明确失败,提示开发者必须先在本地格式化。flake8本身只做检查所以没有这个问题。这一个小策略能确保仓库里的代码永远符合规范,也是在团队协作中最稳妥的方案。

5. 在 VS Code 和 PyCharm 中配置保存即格式化

5.1 VS Code:开箱即用但注意默认格式化器

VS Code 是很多 Python 开发者的主力编辑器。Python 扩展本身就内置了对 Black 的支持,但有个前提:你需要在设置里把默认格式化器改成 Black。不然 VS Code 默认用的可能是 autopep8,风格跟 Black 不匹配,保存之后格式反而乱了。

具体操作分两步。第一步:在设置里搜索 “Default Formatter”,选择 “Black Formatter”。如果是新版本,直接安装官方 “Black Formatter” 扩展,然后在文件类型里把 Python 的默认格式化器设为这个扩展。第二步:打开 “Format On Save” 选项。这一步设置完,每次 Ctrl+S 都会自动调用 Black 进行格式化。

在settings.json里对应的配置长这样:

{ "editor.formatOnSave": true, "python.formatting.provider": "black", "[python]": { "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnSave": true } }

这里有个细节值得注意:VS Code 的 Black 扩展会读取项目根目录的pyproject.toml,所以项目级的配置仍然有效,不会因为你换编辑器就丢。

5.2 PyCharm:通过外部工具集成更顺滑

PyCharm 同样可以直接用 Black,最简单的是通过 File Watcher 插件或者 External Tools 配置。我的习惯是配置一个外部工具,映射到一个快捷键上,比如Ctrl+Alt+B,想格式化的时候手动触发,避免在调试过程中代码被频繁保存自动重排导致运行环境“闪变”的困惑。

External Tools 配置方法:进入 Settings → Tools → External Tools,新增一个工具,Program 填black的完整路径,Arguments 填"$FilePath$",Working Directory 填$ProjectFileDir$。这样可以在任意文件上右键直接调用 Black。如果希望在 PyCharm 里也实现“保存即格式化”,可以使用 File Watcher 插件监视*.py文件的保存事件,然后触发外部工具执行。

5.3 有没有必要开“保存即格式化”

“保存即格式化”听起来很方便,但有团队协作时可能会产生负面效果:如果你在一个已经用了不同风格的项目里打开文件,一保存就把整个文件格式化了,diff 会膨胀到难以 review。因此我的建议是:如果你正在新建项目或项目已经全员统一用 Black,那开保存时格式化没问题;如果你只是从别人仓库里拉代码来读一读,那不如用手动触发的方式,只在想格式化时按一下快捷键,别把整个项目搅浑。

6. 在团队项目中推进 Black 的落地经验

6.1 直接全量格式化,还是渐进式推进

老项目里推 Black,第一个要决策的问题是:存量代码怎么办。一种方案是挑个周末直接全部格式化完,提交一个大 diff。优点是以后所有代码风格统一,干净利落。缺点是一旦有人在格式化之后立刻改了文件,review 时会分不清哪些改动是格式化产生的,哪些是业务逻辑变化。

另一种方案是渐进式推进:只对新代码和改动过的文件执行 Black,旧代码保持原样。在.pre-commit里配置 Black 只在暂存文件上运行,效果就是每次提交的新增或修改代码都是格式化过的,旧文件可以留着以后慢慢清理。这种方案更温和,适合代码量庞大、团队协作频繁的项目。我个人偏向于第二种,如果项目代码量小、团队也想借机做一次整体整理,那第一种也不是不行,但要选在开发不活跃的时间窗口。

6.2 如何避免“格式化一次,代码全乱了”的误判

如果不小心对某个巨型文件跑了一次 Black,你可能会看到上千行 diff,心里一慌。这事不用怕,因为这种一次性变化是可以接受的,反而在 git 里产生一个清晰的分水岭。我的经验是,在做这种全量格式化的提交时,把git diff --stat先跑一下,记录统计信息,然后在 PR 描述里明确写“纯格式化提交,不包含逻辑修改”,评审的人就不会被大 diff 吓到。

更稳妥的验证方式是,在格式化之前记录下测试结果的快照,格式化之后重新跑一遍测试,两者完全一致即可。或者利用git stash临时切换格式化和未格式化的版本,各跑一遍测试做对比,虽然耗时但能极大减少心理负担。

6.3 代码规范这件事一定写进 README

工具配置好了,还得让人知道。README 里加一个“开发环境”章节,写清楚本项目的格式化命令:

make format # 或者 black . && isort . && flake8 .

然后说明清楚:提交代码前必须跑pre-commit,否则 CI 会挂。有了文档和 CI 双层保障,新同事上手的时候会非常省心。很多项目失败就失败在“口头约定规则”,这比没有规则更糟糕,因为它是隐性的。把规则显式地写下来、自动化跑起来,才是团队工程化的标志。

7. 使用 Black 过程中最常见的坑与排查技巧

7.1 格式化后测试挂掉是怎么回事

这种情况虽然少见但并非没有。最常见的原因是测试代码里用了 doctest,而 doctest 的输出字符串对空格和换行很敏感。Black 对注释和字符串内容不做重写,但如果格式化改变了输出示例周围的缩进,doctest 可能就会挂。解决办法就是把 doctest 的文本放好,或者把容易受格式影响的测试移到普通测试函数里。另一个可能原因:某些测试依赖源代码文件的行号,比如报错信息快照、覆盖率报告。格式化必然导致行号变化,这类测试也得跟着调整。

7.2 Black 与 Notebook 里的代码

Jupyter Notebook 的.ipynb文件本质是 JSON,Black 原生不支持直接格式化 Notebook 内部代码。需要借助nbqa这个工具,用法类似于:

nbqa black your_notebook.ipynb

它会把 Notebook 里的每个代码单元当作独立的 Python 代码去格式化。注意这有一个细节:Notebook 本身允许代码单元之间共享全局状态,Black 在格式化单元时并不会感知跨单元的变量,它只做语法层处理,所以安全性没问题。vscode 里也有对应的 “Notebook Cell” 格式化支持,但效果不如nbqa稳定,需要自行取舍。

7.3 如何调试“为什么这么格式化”

Black 内部有一套复杂的格式化策略,有时候看着结果很奇怪,想知道原因。它有一个隐藏参数--fast和--safe控制执行速度与安全检查,还可以用--verbose来输出更多诊断信息。不过更好的办法是拿一个小例子,逐步构造出“难看”的格式,然后跑black --diff看它怎么改,再反过来推断规则。通过这种方式,你对 Black 的“脾气”会把握得越来越准。如果实在不理解,就去翻官方文档中关于 “code style” 的章节,那里有最权威的格式决策说明。

7.4 配置了 pyproject.toml 但没生效怎么办

经常有人遇到这种情况:明明在pyproject.toml里写好了[tool.black],跑 Black 却还是默认行为。原因大多数是 Black 的版本太低,22.0 之前对 pyproject.toml 的支持比较弱,新版本才行。另一个容易被忽略的点是:如果你在子目录里运行 Black,它不一定能找到根目录的pyproject.toml。Black 是从当前运行目录开始向上搜索的,所以如果配置文件放在项目根目录,确保你在项目根目录里运行命令。这个坑我踩过不止一次,后来干脆在 Makefile 里固定一条命令,在根目录执行,避免人为误差。

8. 最后的配置建议:一份可以直接抄的模板

如果你所在团队还在犹豫要不要上 Black,我建议别想太多,直接用起来。先小范围实验,挑一两个模块文件跑一遍,感受一下差异,再决定全量是否推广。下面分享一份我的通用模板,它适合大部分中小型 Python 项目:

[tool.black] line-length = 88 target-version = ['py38', 'py39', 'py310'] extend-exclude = ''' /(build|dist|\.venv|venv|generated)/ ''' [tool.isort] profile = "black" line_length = 88 known_first_party = "your_package_name" [tool.flake8] max-line-length = 88 extend-ignore = "E203,W503"

配合.pre-commit-config.yaml以后,这个模板基本就是业界比较标准的现代 Python 项目格式规范底座了。我自己的经验是,这套组合跑起来之后,“格式化”这件事就从日常心智负担中彻底消失了,剩下的精力可以全部放在业务逻辑和架构设计上。

代码格式化看起来是个很小的话题,但它影响的是整个团队每天的协作效率。Black 的流行并非偶然,它帮你做决定、避免争论、降低 diff 噪音,节省的隐性时间远比想象中多。如果你还没有试过,找个下午花十分钟跑一次,配合 isort 和 pre-commit 搭好链路,后续收益会在每一次提交代码时自然体现出来。最后再分享一个小技巧:每次在新环境配置完工具链,跑一下black --check .看是否通过,通过后再开始写代码,能避免后续大部分格式问题的积累。

返回列表