尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Black:终结Python代码风格之争的自动格式化工具

Black:终结Python代码风格之争的自动格式化工具 写这篇文章的起因很直接我见过一个十几人的研发团队在一个Code Review里花了整整二十分钟争论“三个参数的换行到底该不该拆”。那不是黑话梗那是真实发生在周五下午的事。后来我们引入了BlackPython社区里那个号称“让你没有选择”的自动格式化工具争论当场消失。你要知道Black不是那种“帮你把代码整理得舒服一点”的小插件它是一台没有感情的格式化机器拿到代码就按自己的规则重排不管你喜不喜欢。刚接触它的人多半有点嫌弃但真的用过一段时间再回头看手工整理的代码你会感激它的存在。这篇内容我打算把完整的接入过程、设计哲学、实际踩过的坑都写出来给所有还在被代码风格问题困扰的Python开发者一个可以直接抄的答案。1. 风格之争背后的团队成本为什么一个格式器值得较真1.1 你永远说服不了一个坚持单引号的人先说个扎心的事实在代码格式这件事上几乎每个人都觉得自己的习惯才是最合理的。“字符串就该用单引号省一次Shift按键”“双引号才是符合习惯的写法”“缩进必须是四个空格两个空格的退出这个房间”“函数之间必须空两行空一行看起来太小气”。这些争论看起来很小但它们有一个共同特征没有标准答案只有个人偏好。PEP 8虽然是官方风格指南但它自己也说了很多规则只是建议。于是团队里会出现一种诡异的局面——大家明明都知道PEP 8却还是能在“if语句里要不要加括号”这种问题上吵得不可开交。我自己就经历过最典型的一次一个同事的代码里混用了单双引号另一个同事指着其中一行说“你这样写不规范”然后两个人从Python引号规则一路聊到了C语言的字符串设计哲学。最后那个PR花了三个小时才合进去其中至少一个半小时和业务逻辑无关。这就是格式噪音的威力它不产生任何业务价值却消耗团队最宝贵的时间和注意力。而且越是资深的开发者越容易在这种事情上较真因为每个人都有一套打磨了很多年的肌肉记忆。1.2 格式不一致的隐性成本评审、Git历史和新人成长如果只是争吵还好真正让人头疼的是格式不一致带来的隐性成本这些东西平时看不见但每个都在暗处拖慢团队速度。第一是评审效率。一个PR里如果混着“把变量换个名字”“修了一个边界bug”“顺手改了十几处格式”这三类改动评审的人需要反复确认哪些是有效修改哪些只是排版变化。大脑在做这种切换时特别容易漏掉真正的逻辑问题。第二是Git历史污染。你打开git log -p想找某个功能是哪次提交引入的结果屏幕上一整屏都是因为某天有人格式化过文件而产生的空白差异。真正的代码改动淹没在换行和空格的噪音里追溯问题变得极其困难。第三是新人学习成本。一个刚加入团队的人打开项目看到四种不同的风格并存他第一反应往往不是“团队没有规范”而是“这个项目是不是没人维护”。他会纠结自己写代码时该跟谁学最终写出来的又是第五种风格。这些问题不会在第一天爆发但会随着团队和代码库规模的增长持续累积。我后来总结了一句话手动统一风格这件事本质上是在用人的注意力去干机器该干的活。Black的价值就在于它把这块完全接管了。2. Black的设计哲学它凭什么“固执”到号称不可争论2.1 不可配置的设计少即是多如果你用过YAPF或者autopep8应该知道这类格式化工具给了你一大堆配置项对齐方式、换行阈值、缩进宽度、空行数量……看起来是好事对吧想怎么调就怎么调。但这里有一个被忽视的悖论只要一个工具是可配置的团队就必须为配置方案再吵一轮。你引入了统一格式化的工具结果变成了引入了统一的纷争——只不过争论对象从“代码该长得什么样”变成了“工具配置该怎么设”。Black直接把这个门关死了。它的可配置项少得可怜行宽默认88、目标Python版本、是否规范化字符串引号、是否跳过字符串规范化。就这几个没了。你没法设置“参数对齐模式”没法设置“二元运算符之前还是之后换行”没法设置“函数定义后空几行”。Black的作者在文档里写得很明白他不希望你把它变成一个需要做决定的工具他希望它是一个只需要执行的决定。官方甚至用了这样一个表述“你只需要付出一节课的时间来学习它的规则之后你永远不会再为代码风格做决策了。”我最初觉得这个设计太霸道凭什么替我做主但实际用了两周之后想法完全变了。它就像一个城市的交通规则你不会每天在路上纠结“为什么红灯要等三十秒而不是二十秒”你只会因为所有人都在遵守同一套规则而觉得安全。需要做决定的时候越少留给真正重要事情的时间就越多。2.2 确定性、幂等性和88字符的来历Black有两个被反复强调的性质一个是确定性一个是幂等性。确定性是指同样的输入在任何环境下跑Black输出永远一致。不会因为你用的是macOS还是Windows不会因为Black是小版本还是大版本就产生差异。这意味着格式化的结果可以像编译产物一样被信任。幂等性是指格式化一次之后再格式化第二次结果保持不动。有的工具不能做到这一点——第一次格式化后会生成一种布局再跑一次又会变成另一种布局这就很麻烦因为你永远无法判断“现在的代码到底被格式化干净没有”。Black是设计上强制保证幂等的这看似是个小细节实际体验天差地别。至于那个88字符的默认行宽很多人以为是从PEP 8的79字符直接拍脑袋来的。实际上Black的作者在博客里解释过79这个限制来源于早期终端物理宽度的约束今天已经不适配大部分屏幕了。他们参考了一些实测研究选择了88这个折中值——既能保证在分屏和宽度较小的编辑器中不出现严重换行又比79多出了将近一格的实际可用空间。你说它是科学也行说它是审美也罢关键是它对所有人一视同仁没有商量余地。2.3 Black管什么不管什么很多刚开始用Black的人会有一个误解代码交给Black格式化之后就万无一失了。这是把格式化工具和代码检查工具搞混了。Black只管排版。它管的是这一行超过88字符怎么断函数参数列表该怎么排括号该不该保留字符串该用单引号还是双引号空行该怎么控制。它不会替你发现未使用的变量不会提醒你某个函数复杂度太高更不会在你写出一个无限循环时拦下你。换句话说Black是“文字排版师”不是“编辑”。你仍然需要配套的静态检查工具去处理逻辑层面的问题比如Ruff、Flake8、Pylint、Mypy这类工具。它们在团队里的定位完全不同Black负责让代码看起来统一静态检查负责让代码更健壮。这个边界搞清楚之后你才不会对Black产生不切实际的期待也不会因为“Black没抓出某个bug”而失望。3. 从命令行到CI接入Black的完整落地记录3.1 环境准备与安装版本锁定的重要性在安装Black之前先确认你的Python环境是干净的。如果你连Python都还没装好那先去搞定环境再回来Windows用户从官网下载安装包macOS可以用Homebrew装Linux用户用系统包管理器或者源码编译都可以总之把Python 3.9以上版本准备好就行。安装Black本身很简单一行命令pip install black如果你用的是Poetry或者pipenv建议作为开发依赖安装poetry add --dev black # 或者 pipenv install --dev black有一个非常关键的建议在项目里锁定Black的版本。Black的格式规则会随版本迭代微调——同一个文件用23.x和24.x格式化出来的结果可能不完全一样。如果不锁版本今天你本地格式化一遍明天CI上的Black升级了又改一遍文件就被反复折腾。锁版本的方式很简单在你的requirements-dev.txt或Poetry的pyproject.toml里写死版本号或者用pre-commit的rev字段指定一个确定的release tag。之后所有人和CI都使用同一版本格式化结果才能稳定。我见过不锁版本导致CI和本地结果不一致的案例排查起来非常难受。3.2 三条核心命令format、check、diffBlack的命令行使用成本很低普通人只需要记住三条。第一条是直接格式化文件或目录black src/ tests/这条命令会直接改写src/和tests/下所有Python源文件。注意它是原地修改不会输出一个“格式化后的副本”。所以跑之前最好有个干净的工作区有问题还能用git回滚。第二条是检查而不修改用于CI集成black --check src/ tests/这条命令只检查文件是不是已经符合Black规则如果发现需要格式化的文件会报错并列出文件名但不会改动任何内容。它的退出码也很有用全部合格返回0存在不合格文件返回非0正好适合扔给CI当门槛。第三条是预览差异black --diff src/ tests/这条命令会把“当前文件”和“格式化后应该长什么样”的差异输出到终端像是一个简化版的git diff适合在真正格式化之前先看一眼Black会怎么改你的代码。我的习惯是本地先跑black --diff看差异合不合理确认没问题后再跑black直接修改最后提交时再顺手跑一次black --check确保没有丢失。3.3 配置文件的正确写法pyproject.tomlBlack官方推荐把配置放在pyproject.toml里好处是配置文件统一不用再为新工具单独创造一个setup.cfg或.black文件。给一个我实际在用的配置示例[tool.black] line-length 88 target-version [py38, py39, py310] include \.pyi?$ exclude /( \.eggs | \.git | \.venv | build | dist )/ extend-exclude /( migrations | generated_proto )/ 逐项说下我的理解line-length行宽阈值默认就是88如果团队没有特殊需求建议保持默认不要为了一两个人的显示器宽度去改。target-version目标Python版本Black会按照这些版本的语法特性来决定某些格式化策略。include/exclude哪些文件参与格式化用正则表达式匹配。exclude里我把依赖目录、构建产物都挡在外面避免Black去动虚拟环境和打包目录里的文件。extend-exclude这是给项目遗留代码准备的。比如我们有一个正在逐步弃用的migrations目录和自动生成的generated_proto目录暂时不纳入格式化范围。它的优先级高于exclude适合用来设置“我知道这里还没格式化但暂时不能动”的名单。还有一个特别实用的配置叫force-exclude。它在命令行层面也生效就算有人手动指定了某个排除的目录也能拦住。适合放在和CI共用的配置里防止有人一时手快格式化整个仓库。3.4 与编辑器、pre-commit和CI的联动光会命令行还不够让Black真正“无感化”需要把它接入编辑器、提交钩子和CI里。在VS Code里装好Python扩展后再加装Black Formatter扩展然后在设置里指定默认格式化器{ editor.defaultFormatter: ms-python.black-formatter, [python]: { editor.defaultFormatter: ms-python.black-formatter, editor.formatOnSave: true } }这样每次保存文件Black会自动帮你格式化。和Git钩子配合从源头上杜绝未格式化代码进入版本库。PyCharm的话需要把Black注册成外部工具Settings → Tools → External Tools添加一个工具Program填Black的安装路径Arguments填$FilePath$Working directory填$ProjectFileDir$。这样你就能给当前文件手动执行格式化。PyCharm新版也支持通过插件自动集成但外部工具这种方式兼容性最稳。pre-commit是另一个重要环节项目根目录的.pre-commit-config.yaml里加上repos: - repo: https://github.com/psf/black rev: 24.1.1 hooks: - id: black之后每次git commitBlack会在提交前自动检查暂存区的Python文件如果有未格式化的代码就拦住提交并直接帮你改好然后你重新git add再提交就行。CI里更简单直接在流程里加一段black --check --diff .--check保证CI不会去修改代码--diff让CI日志里直接输出差异方便开发者看到到底哪里不符合规范。三步下来——编辑器保存时格式化、提交前钩子拦截、CI最终兜底基本可以做到仓库里永远只有格式化干净的代码。4. 实测Black的改码规则那些让代码大变样的重排细节4.1 长函数的参数列表Black的换行策略Black最经典的改动就是对超长单行的拆解。拿这段代码举例def process_order(user_id, product_id, quantity, promo_code, coupon_type, callbackNone, notifyTrue): return execute(user_id, product_id, quantity, promo_code, coupon_type, callback, notify)第一行明显超过88字符。手工整理时不同人有不同排法有人全部写到一行里凑合有人分成两行对齐左括号还有人每行放两个参数。Black的处理方式非常统一def process_order( user_id, product_id, quantity, promo_code, coupon_type, callbackNone, notifyTrue, ): return execute( user_id, product_id, quantity, promo_code, coupon_type, callback, notify, )注意几个细节首先是每个参数独占一行而不是“尽量多塞几个直到塞不下”其次是括号内的最后一个参数后面带了一个魔法逗号最后是函数调用同样被拆开了。很多人第一次看到这种风格觉得占行太多、太浪费。但实际用过再看会发现每行一个参数在评审时特别友好增删参数时diff非常干净多一个参数就是多一行少一个就是少一行不会出现“某个参数被挤到下一行导致diff里同时出现删除和新增”的混乱情况。4.2 表达式重排、括号的去留和布尔运算连写Black对长表达式也很有一套。比如下面这段代码if a 10 and b 20 and c 30 and d ! 40 and e is not None and f in allowed_set:一眼看过去全是and逻辑本身没问题就是太长了。Black会做类似这样的转换if ( a 10 and b 20 and c 30 and d ! 40 and e is not None and f in allowed_set ):这里有个很多人容易忽略的细节Black在拆表达式时倾向于把二元运算符放在行首。也就是说它选择“运算符之前换行”而不是“运算符之后换行”。这个选择是有意为之的因为行首的运算符能让你一眼看出这行是一个逻辑链条中的一环尤其适合阅读连续and/or的条件判断。另外一个常见改动是自动补括号。碰到超长的返回值或赋值表达式Black会主动添加括号然后换行比如return some_var another_var yet_another_var final_long_variable_name_here会被改成return ( some_var another_var yet_another_var final_long_variable_name_here )表面上看代码变长了但每一步运算的组成部分都清清楚楚。遇到调试需求时你可以直接在中间某个变量后面打断点不用再去数“第几个加号后面是谁”。4.3 引号统一与魔法逗号两个最容易被误会的规则Black默认会把字符串引号统一成双引号比如name Alice message hello world格式化后变成name Alice message hello world但这里有一个例外逻辑Black会为了避免转义而保留单引号。比如某个字符串里已经出现了双引号再强制用双引号就得写成He said: \hello\反而更难看。这种情况下Black会自动选择单引号包裹。这个规则很实用它追求的是“不管用哪种引号尽量让屏幕上少出现反斜杠”。再来说魔法逗号。这个词听上去很玄实际上是一个确定的规则当多行结构的最后一个元素后面带有逗号时Black会认为你希望维持这个结构的多行展开状态即使它足够短可以被压缩成一行也不会去压缩。举个例子。这段代码因为带了一个尾部逗号Black会保留每行一个参数result compute_something( param_a, param_b, )而如果你去掉最后一个参数后面的逗号并且这行的长度允许Black就会把它压缩成一行result compute_something(param_a, param_b)这个规则知道后特别有用。它给了你一种手动控制格式化结果的“暗号”想保持展开就保留尾部逗号想压缩就删掉它。虽然Black表面说自己不容商量但它实际上留了这个后门只是很多人不知道。5. 团队推广Black时踩过的坑与验证过的经验5.1 最大的坑全仓库一次性格式化如果让我只给一条关于Black落地的建议那就是千万不要在一个PR里对整个仓库跑一遍Black然后直接合并。为什么因为全仓库格式化会让git blame彻底失去意义。每个文件的每一行看起来都被改过了排查历史责任时git blame会把大半个项目都指向那个格式化PR。更糟糕的是如果同时有多个功能分支在开发格式化PR很容易产生大范围合并冲突冲突解决起来能把人逼疯。正确做法是分批次推进。我们的实践是这样的先把Black接入CI和pre-commit让全仓库的“新改动”全部遵循Black规范。老文件用extend-exclude先排除比如先排除legacy/和vendored/这类模块。一个目录一个目录地消化每个目录单独开一个格式化PR合并时避开功能分支的活跃期。优先格式化正在开发的模块停止维护的老模块可以先放着不要碰。这样做了大概两个月之后我们仓库里被排除的目录范围越缩越小最终几乎整个代码库都完成了格式化而且全程没有出现过一次大规模合并事故。格式化这件事就像大扫除你不能一口气把整个屋子翻个底朝天要一个房间一个房间来。5.2 Black与isort、Flake8的协作细节Black只处理格式另外两个工具需要配套调整import排序用isort静态检查用Flake8或者Ruff。它们之间协作时有两个已知的坑直接给你看配置解法。isort负责把import按规范排序但isort默认的换行策略和Black不一样。解决方式是让它使用Black profile[tool.isort] profile black line_length 88第二坑是Flake8的E203规则。Flake8默认会检查切片操作符:前后的空格但Black的某些格式化结果特别是在切片里的冒号周围留空格以对齐时会被E203误报为错误。我们需要在Flake8的配置里显式忽略[flake8] extend-ignore E203, W503W503是另一个和“二元运算符换行位置”有关的规则Black选择了运算符放在行首而W503默认要求运算符放在行尾所以也得关掉。这个坑不填的话你会看到CI上Black检查通过后又冒出一堆Flake8报错两家互相打架。配置对齐之后它们就变成了互补关系Black管形状isort管顺序Flake8管质量问题。5.3 格式化PR和功能PR分离以及git log -w的补丁技巧前面说到不要全仓库格式化其实更细的规矩是格式化的PR和功能改动的PR永远不要混在一起。一旦你修了一个bug顺手格式化了文件评审者就很难分辨哪些改动是逻辑修复哪些只是排版变化。正确的做法是格式化PR单独开、单独标明[style]前缀功能PR保持逻辑改动纯粹干净。如果你已经有了一堆混合的历史提交有一个小技巧可以补救git log -p -w。加一个-w参数Git会忽略空白字符的差异只显示真正的代码改动。在审查历史时非常好用能让你从一大堆格式噪音中快速找到实际变更。还有一件事要提前有心理准备Black的版本升级会导致已有代码被二次格式化。比如Black 23.x和24.x对某些结构比如带魔法逗号的多行表达式的处理会发生变化。升级时要像对待框架版本升级一样对待先锁定新版本跑一遍black --check --diff把差异控制在可接受范围内再提交。不要跟进每个新版本除非你确实需要它带来的新功能。5.4 把格式争论挤出有限带宽半年的真实体会接入Black半年后再回头看最大的变化不是代码变好看了而是整个团队的注意力被重新分配了。代码评审开始讨论逻辑边界、异常处理、性能瓶颈这些真正需要人类判断的东西而不是花时间争论“这里为什么是空两行而不是空一行”。新人入职第一天我们只告诉他一句话装好编辑器保存时它会自动用Black你不用管格式。从那天起他没有问过任何一个格式问题。如果你问我个人对Black的态度我的回答是它不是最好的格式器它的很多选择我也不完全认同。但它把“风格一致性”变成了一个完全不需要讨论的默认项这本身就是它最大的优点。代码风格本来就该由机器来强制执行人脑的带宽应该留给真正的逻辑问题这才是我推荐你用它并坚持用下去的真正理由。
返回列表