多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python代码格式化神器Black:从入门到工程实践

Python代码格式化神器Black:从入门到工程实践 1. 为什么我建议每个Python项目都引入Black先聊点实在的。如果你写过一段时间Python大概率经历过这样的场景项目里每个人的代码风格都不一样有人喜欢单引号有人喜欢双引号有人习惯在运算符两边留空格有人不留有人一行能塞一百多个字符也不换行。Code Review的时候光是争论“这里该不该换行”“这行是不是太长了”就能耗掉半小时真正的业务逻辑反而没时间看。后来我接触到Black官方自嘲叫“uncompromising formatter”翻译过来就是“不妥协的格式化工具”。什么意思就是它根本不跟你商量只要格式不符合它的规则直接一把梭给你改到位。我刚上手的时候其实有点抵触毕竟写了这么多年代码突然有个工具告诉你“你的风格不对按我的来”心里多少有点别扭。但用了一个月后我彻底真香了现在新开的项目无一例外全部接入Black。这篇文章我不打算念文档我会结合自己实际项目里的使用体验把Black怎么装、怎么用、怎么和IDE还有CI流程配合以及我从坑里爬出来的经验一口气讲清楚。如果你正在纠结要不要在团队里推行统一代码风格或者你只是一个人写项目但想省掉手动调格式的精力这篇文章应该能帮到你。Black说到底就解决一件事让你的代码格式自动化。它不像PEP 8那样只是一份建议文档它是一段真实的程序你跑一下它格式就变规范了。这种“机器说了算”的思路看起来粗暴实际效果却非常好因为它从根本上消灭了“风格争论”这个人际矛盾。2. Black的核心设计理念与优点拆解2.1 Black凭什么敢说“不妥协”先说结论Black的格式化策略核心就一条——尽量减少代码的diff量。这个理念很有意思它不一定要生成“最美”的代码但它要生成“最稳定”的格式。什么意思你用手工或其它工具格式化出来的代码下次改一行逻辑可能整个函数都被重新排版了Git diff上全是格式变动根本看不清这次提交到底改了什么。而Black的规则是经过大量真实代码库统计和实验得出的能最大化保证格式的稳定性改一行就只diff一行。另一个让Black这么有底气的原因是它的实现非常严谨。它先把你的代码解析成抽象语法树AST再做格式化输出而不是那种简单的“正则替换”。举个典型例子def foo(a,b):return ab这行代码在Black手里会变成def foo(a, b): return a b看清楚没有逗号后面加了空格函数定义和函数体分了行运算符两边留出空格。为什么能处理得这么干净因为它先解析了语法结构知道哪个token是参数分隔符哪个token是运算符然后按规则重新生成。如果是正则替换碰到字符串里的逗号就傻眼了。从这一点就能看出Black处理代码是建立在真正的语法理解之上所以它不会破坏你的代码逻辑这是底线。2.2 对比其它工具Black、autopep8、yapf怎么选很多刚接触自动格式化的人会问市面上不是有autopep8和yapf吗为什么非要用Black我三个都试用过做个对比供你参考。工具核心风格可配置性diff稳定性上手难度autopep8严格遵循PEP 8较高一般低yapf支持多种风格预设高一般中Black自成一派类PEP 8极低极高低说直白点autopep8的问题是选项太多了团队里的每个人都能调出自己的一套配置效果最终还是看人。yapf风格虽然多但配置复杂度上去了维护成本高。Black把配置项砍到只剩几个默认配置开箱即用几乎做到了“零讨论”。在团队协作场景里少一个可选项就少一个吵架的理由这就是Black最大的价值。而且Black格式化的结果和PEP 8在大方向上是一致的只是个别细节更严格或更统一。比如PEP 8说一行最长79个字符但很多团队实际用的时候觉得太短老是换行Black默认写88个字符这是一个经过测算的平衡点既能保证代码不过分拥挤也不会像79那么频繁换行。我用下来88这个数字挺舒服的如果你有特殊需求后面我会讲怎么改。2.3 它不会动你哪些代码要打消大家的顾虑也得说清楚Black的边界。它再强硬也有一些东西是它不会碰的。字符串内容它不会动你写什么照样保留什么。注释内容它也不会动但它可能会把注释的位置调整一下比如从行尾移到代码上方。另外变量名、函数名、import路径、print输出的内容这些它都不碰。这里有个很有意思的细节如果你把一些特殊标记写在字符串里Black也不会动它。比如你在一些库里面见过这种写法# fmt: off在这个注释后面、# fmt: on之前Black会跳过整段代码的格式化这在某些手写对齐的矩阵或者表格类代码里非常实用。后面我会专门讲这个功能。3. 安装与基础使用从零开始跑通Black3.1 安装方式和版本选择装Black非常简单常规的pip方式就够了。pip install black如果你用的是Python 3.10以上版本建议直接装最新版因为新版本的语法解析能力更强对match-case这类新语法支持也更好。要是你和我一样平时用虚拟环境就在项目虚拟环境里单独装一份。有一点提醒你Black有Python版本要求不同版本的Black支持的语法不完全一样。如果你项目里用了特别新的语法特性建议把Black升级到最新版本不然它会直接报错提示不支持某段语法。我遇到过几次这种问题升级之后就好了。3.2 第一次格式化命令行实操演示装好之后先找个测试文件跑一下。假设我有一个叫demo.py的文件内容乱糟糟的x{a:1,b:2} def hello(name):print(Hello,name)直接在终端执行black demo.py输出大概长这样reformatted demo.py All done! ✨ ✨ 1 file reformatted.再打开文件看看内容已经变成了x {a: 1, b: 2} def hello(name): print(Hello, name)注意看几个细节。字典里冒号后面加了空格但冒号前面不加空格这是PEP 8的标准风格。函数定义后面自动空出了两行这是PEP 8规定的顶层函数之间要空两行。这些都是Black自动处理的你不用再自己数空行。3.3 常用命令参数一览Black命令行的常用参数我整理成表格平时用这几个就够了。参数作用black 路径格式化文件或目录--check只检查不修改CI场景常用--diff显示格式差异不真的修改文件--line-length 数字设置行长度--skip-string-normalization保留原有的引号风格--target-version py310指定目标Python版本-S--skip-string-normalization的简写--fast跳过AST安全检查速度快一点--check和--diff这两个参数在持续集成里非常有用。我之前在跑CI的时候经常用它来拦截那些没有格式化就提交的代码。后面讲workflow集成的时候会示范完整用法。3.4 检查格式化是否生效有时候改了配置不确定当前代码是否符合规范可以用black --check --diff .这个命令会扫描当前目录下所有Python文件如果发现不符合格式的地方会直接显示出差异内容但不会真的改文件。你看一眼差异大概就能判断要不要重新跑一次完整的格式化。这个组合拳我几乎每天都用排查问题非常效率。4. 与主流开发环境集成让格式化融入日常4.1 VS Code集成插件配置如果你用VS Code写Python安装Black的体验非常顺滑。首先在扩展市场搜Python插件装好后VS Code已经内置了格式化工具选择功能。操作路径是这样的设置里搜索formatting找到Python格式化工具的相关选项把默认的autopep8换成black。或者直接在项目的.vscode/settings.json里写上{ python.formatting.provider: black, editor.formatOnSave: true, editor.formatOnType: false }我在 python.formatting.provider 这里吃过一个亏早期版本里VS Code用的是这个字段后来新版推荐用editor.defaultFormatter直接指定整个编辑器默认格式化器。但如果你项目里有其它语言的代码不要把editor.defaultFormatter全局设成Black不然格式化别的语言文件会出问题。我现在的做法是只在Python工作区里配好其它文件让原生的格式化器来处理。editor.formatOnSave记得打开这个功能就是文档里说的“保存即格式化”。你写完代码CtrlS一按Black自动跑一遍格式就整齐了。这个配置对新手特别友好完全不用记命令。4.2 PyCharm/IntelliJ IDEA的配置方法PyCharm用户也不慌配置Black分两步。首先装好Black然后打开PyCharm的Settings找到Tools下的External Tools点加号新建一个外部工具。Program那里填Black的路径Windows下通常是C:\User\你的用户名\AppData\Local\Programs\Python\Python310\Scripts\black.exe如果你不确定路径在PyCharm的Terminal里跑which blackMac/Linux或者where blackWindows就能看到完整路径。Arguments填$FilePath$Working directory填$ProjectFileDir$配置好之后你可以给这个外部工具绑定一个快捷键比如AltB以后按一下快捷键当前文件就会被Black格式化。还有一个小技巧Windows下面的路径经常有反斜杠和空格一定记得给Program路径加上英文双引号括起来不然PyCharm会报错找不到程序。这个坑我踩过一次困扰了我好久才找到原因希望后来的朋友不用重复踩。4.3 pre-commit钩子提交前自动检查真正进阶的玩法是在Git提交之前自动跑一遍格式化检查。这块我强烈推荐用 pre-commit 这个框架它专门用来管理各类代码检查钩子。安装pre-commit同样很简单pip install pre-commit在项目根目录建一个.pre-commit-config.yaml里面写上repos: - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black language_version: python3然后在终端跑pre-commit install这一步会在你的.git/hooks目录生成钩子文件。以后每次git commit提交的时候pre-commit会自动检查暂存区里的Python文件如果格式不对它会自动帮你改好然后提交就会被拦截。你需要做的就是重新git add那些改动过的文件再提交一次。我自己的流程是这么设计的日常写代码依赖IDE的保存即格式化到了提交这一步pre-commit兜底检查双重保险基本不会出现格式不干净的情况。如果哪次CI跑挂了提示格式问题我也不会去手动改直接跑一下black .一句命令解决。4.4 搭建GitHub Actions自动检查如果你用GitHub托管项目还可以加上CI检查。在.github/workflows/lint.yml里写name: Lint on: push: pull_request: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-pythonv4 with: python-version: 3.11 - run: pip install black - run: black --check .这段配置的意思就是每次push或者提交pull request的时候服务器上会自动拉取代码装好Black然后跑一遍black --check .。如果代码格式不符合规范CI状态就是失败的PR页面会直接显示红色叉号。这种“机器把关”的方式比我人工催同事改格式化有效一百倍毕竟谁都不想看着自己的PR被挂在那里红彤彤的。5. 配置文件与高级定制细节里的门道5.1 使用pyproject.toml统一项目配置Black支持通过pyproject.toml来配置这个文件通常放在项目根目录。比如这样[tool.black] line-length 100 target-version [py310] include \.pyi?$ extend-exclude /(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|svn|__pycache__)/ 我习惯把行长度调到100因为我们团队显示器普遍偏大88有时候还是会过早换行。这里要特别注意target-version不是说我用Python 3.10它就只会识别3.10的语法。这个参数的含义是Black会按Python 3.10的语法规则去解析代码同时生成的格式也兼容3.10。如果你项目还在跑Python 3.8别把target-version写高了否则Black可能会使用一些旧版本解析不了的新语法格式导致代码在低版本环境直接跑不起来。extend-exclude这个字段是用来排除目录的写法和正则表达式一致。默认情况下Black本身就会忽略一些常见目录比如.venv、.tox这类但如果你有自己的目录要排除直接加进去就行。5.2 字符串引号风格用还是不用S参数Black默认会把字符串统一成双引号但很多人喜欢的是单引号。我一开始也不习惯觉得Python社区用单引号的人更多。后来发现Black团队做出这个选择是有理由的大部分语言里字符串默认用双引号Python用双引号写的代码在切换语言时习惯更连贯而且双引号和字符串插值语法在一些场景下兼容性更好。如果你实在无法接受可以加一个参数-S也就是--skip-string-normalization。加了这个参数之后Black不会动你的字符串引号你原来写单引号就保留单引号。我个人的建议是新项目直接沿用Black默认的双引号毕竟这是整个生态的工具链默认值没必要跟默认值对着干。老项目为了控制diff量可以先用-S过渡等时机成熟再切换。5.3 fmt: off与fmt: on局部控制前面提到过的# fmt: off和# fmt: on这里展开讲一讲。有些代码天生需要手工对齐比如字典里按列排好的数据# fmt: off mapping { very_long_key_name_a: {x: 1, y: 2}, key_b: {x: 3, y: 4}, } # fmt: on这段代码手工对齐后非常便于阅读但自动化格式化器通常会把它打散成普通的一行一组反而难看。通过# fmt: off你可以告诉Black“这一段你别管我”。注意# fmt: off必须放在需要跳过的代码之前单独占一行不能写在代码同行行尾。有个细节要提醒fmt: off和fmt: on之间的代码Black会完全跳过不会做任何修改。所以如果你在里面写了严重不符合规范的代码它也会姑息。这个功能是给“特殊情况”准备的别当作偷懒的借口。平时尽量让Black全权负责只在真正需要精细排版的地方使用这个开关。5.4 魔法逗号一个少有人提但很重要的细节这个知识点是我实际用了好久才注意到的就是“魔法逗号”。当你写的列表、元组、字典很长需要换行时如果最后一个元素后面跟了逗号Black会认为你想把所有元素都展开成一行一个items [ first, second, third, ]而如果你把魔法逗号去掉它又有可能压缩成一行items [first, second, third]这个行为在当你调整列表元素数量时尤其重要。你加了一个元素忘记在最后一个元素后面加逗号Black可能不会把列表完全展开diff看起来就会很奇怪。理解这个逻辑之后你能更好地掌控Black的排版行为减少“为什么它把我的列表压成一行了”这类困惑。6. 常见问题与踩坑记录6.1 格式化了但退出码不为0经常会有人问我明明格式化成功了为什么命令行的退出码显示1或者123这是Black故意设计的格式化成功的退出码是0但如果有文件内容发生变化它会返回非0的退出码。--check模式下只要有任何文件不符合格式退出码就是1。这就导致在CI里用的时候经常看到一个很怪的现象代码格式已经被修好了但CI还是失败。其实这不是CI配置错了而是Black有意为之。解决方案很简单在CI流程里先跑一遍black .格式化再跑black --check .检查或者直接用前面说的pre-commit因为pre-commit会自动把改好的文件重新暂存不需要手动处理退出码的问题。6.2 超大文件格式化耗时过长Black默认有一个AST安全校验机制就是每次格式化后它会把格式化前后的代码都转换成AST对比一下确认没有逻辑变化。这个步骤在绝大多数情况下很快但如果你碰到超大文件比如几千行的配置或数据文件运行时间会明显变长。如果你确认自己的代码是安全的可以加--fast参数跳过这个安全检查。我自己是这么处理的正常代码不加--fast保持安全只有处理那种一次性生成的大型数据文件时才会用。这里多说一句不要让--fast成为默认习惯因为安全校验是对代码逻辑的最后一道兜底保障能保住你的代码不被改坏。6.3 格式化结果导致程序行为变化这个情况比较罕见但我确实听说过有人遇到过。最常见的场景是格式化之前代码里依赖了某些隐式行为比如没有空行时两个不同的语句块靠得非常近格式化之后中间多了空行导致某个反射机制或者日志行为发生变化。严格来说这不是Black的错因为真正的问题藏在代码本身只是格式化把这个隐患暴露出来了。如果你遇到这种情况第一反应不要慌张。先看diff确认Black只改了空格、空行、引号这些方面并没有动逻辑。然后用# fmt: off把有问题的代码段包起来做局部处理。另外记得跑一遍你的测试用例无论格式化器多成熟提交前跑一遍测试永远是最可靠的证明手段。6.4 与其它格式化工具冲突项目里如果同时接了isortimport排序工具、flake8代码检查工具这些它们之间的配置偶尔会打架。最常见的是isort和Black关于import排序方式的分歧。isort默认会把import按字母表排而Black要求import块里每行之间用空行做分隔两者配合不好会产生互相覆盖的尴尬。解决方案是给isort加一行配置profile black让isort明确使用Black兼容模式。这个配置的好处是isort会按照Black的规则来排版import语句两个工具不再是敌对关系而是合作把代码整理得井井有条。7. 实测心得我的一次完整接入过程光说碎片经验不行我拿最近重构的一个爬虫项目举例把完整接入Black的过程走一遍你跟着照做基本不会有问题。第一步在项目虚拟环境安装pip install black isort pre-commit第二步在项目根目录创建pyproject.toml写入[tool.black] line-length 100 [tool.isort] profile black第三步创建.pre-commit-config.yaml写入isort和Black两个检查钩子repos: - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort name: isort (python) - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black language_version: python3第四步全项目格式化black . isort .跑完之后我注意到Black和isort把很多文件的顺序调整了一遍。当时diff一拉出来一眼望过去全是格式变动说实话看着有点慌。但确认逻辑没变后我就接受了而且发现从那次之后我的代码一致性明显提升了整个项目的可读性上了一个台阶。第五步安装Git钩子pre-commit install这一套弄完之后后面每天的流程就是写代码、保存、Black自动格式化、提交、pre-commit检查一遍。如果检查挂了说明有小地方露了跑一下black --check看看具体原因快速修掉就好。最后再说一个我个人的习惯代码格式化不是“事后清理”而是“事中常态”。我现在写代码的时候已经不会刻意去调整空格和换行了因为我知道保存的时候Black会自动处理。这大大减轻了写代码时的认知负担我能把更多的注意力放在逻辑本身。如果你还没开始用Black我建议你找一个体量不大的项目先试水跑一个礼拜等习惯了那种“保存即整齐”的体验之后你就再也不想回到手动调格式的时代了。
返回列表