ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/30 0:22:20 拓冰建站 浏览量
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 blackBlack 的版本迭代速度不算慢每个大版本之间格式规则会有微调。比如 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如果想看改动前后的差异用--diffblack --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_argument1, second_argument2, third_argument3)Black 格式化后result very_long_function_name( first_argument1, second_argument2, third_argument3, )这种写法在代码评审里特别受欢迎因为下一次加第四个参数时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 排序交给 isortBlack 不负责整理 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,W503E203是切片空格相关的规则和 Black 的切片空格策略不相容必须忽略。W503是二元运算符换行位置规则Black 坚持运算符在行首因此也要关掉。这个配置是社区公认的“和 Black 兼容”的黄金组合。如果你用的 flake8 版本较老注意extend-ignore字段不是所有版本都支持记得升级。4.3 pre-commit 自动化整个检查流程如果每次提交代码都要手动跑一遍 Black 和 isort总有忘记的时候。我的做法是接入 pre-commit。在项目根目录建一个.pre-commit-config.yamlrepos: - 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” 选项。这一步设置完每次 CtrlS 都会自动调用 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 配置。我的习惯是配置一个外部工具映射到一个快捷键上比如CtrlAltB想格式化的时候手动触发避免在调试过程中代码被频繁保存自动重排导致运行环境“闪变”的困惑。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文件本质是 JSONBlack 原生不支持直接格式化 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 .看是否通过通过后再开始写代码能避免后续大部分格式问题的积累。