ARTICLE DETAIL

资讯详情

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

Python代码格式化神器Black:终结缩进与引号之争

Python代码格式化神器Black:终结缩进与引号之争 写Python这几年我参加过不少code review也见过太多因为缩进、引号、换行这种小事吵起来的场景。直到有次项目里引入了Black一切安静了。Black是一个Python代码自动格式化工具它不跟你商量拿到代码直接按自己的规则重排从根上终结了“代码风格”这种没完没了的讨论。这篇内容就是给所有写Python的人准备的无论你是刚入门的新手还是被存量项目折磨的老手都可以照着这篇文章把Black用起来让格式问题不再占用你的脑容量。1. 从代码评审里的缩进争吵说起Black为什么值得用1.1 Python社区的风格之争与PEP8的模糊地带先聊点背景。Python这门语言有一个知名的官方风格规范叫PEP8它规定了缩进、空行、导入顺序、命名方式等等。问题在于PEP8本身是“建议”不是“强制”很多细节给了人自由发挥的空间。比如一行代码超过79个字符该不该换行、函数参数太长是每行一个参数还是折成两行、字符串用单引号还是双引号这些在PEP8里查不到明确答案。于是每个团队都有自己的“内部规范”每个程序员又对规范有自己的理解code review的时候就会出现大量跟业务逻辑毫无关系的评论“这里应该折行”“这里不该加空行”“引号能不能统一一下”。这些评论耗费心力却没有任何实际产出。我遇到过最极端的一次团队里两个人因为“赋值符号两边要不要对齐”讨论了二十分钟最后谁也不服谁。当时我心里就在想这种问题本该交给工具去解决而不是让人来争执。1.2 Black的定位无需讨论、自动完成的格式化Black就是在这种背景下火起来的。它的作者是Python核心开发者Łukasz Langa出发点非常简单做一个“不妥协的代码格式化器”你给它任何合法的Python代码它都输出一种统一风格的代码没有任何配置选项可以改变这种风格的根本规则。换句话说Black把“风格”从人的手里夺走交给算法。Black本身是开源的可以通过pip直接安装最核心的用法就一行命令black my_script.py。执行之后你会看到文件被重新排版缩进、引号、换行、空格全被统一。它不需要你记住规则也不需要你写复杂的配置文件。它还有一个很贴切的口号——“The Uncompromising Code Formatter”不妥协的代码格式化器。1.3 Black的实际收益diff变小评审回归逻辑引入Black之后最直观的变化是git diff变小了。以前一个小的改动因为触及了大函数diff里可能混入大量重排内容评审者根本分不清哪些是真正的逻辑变化。有了Black之后全队的代码格式统一改动只发生在真正改了逻辑的那几行。另外还有个隐藏收益当你把格式化交给Black你其实是在用工具来保护自己的注意力。你写代码时不用再思考“这个字典是展开写还是压扁写”Black会在你保存的时候替你做决定。评审者也从无意义的格式争论中解放出来把时间留给真正的逻辑。这个收益是隐性的但坚持一段时间后非常明显。在我后来的项目里我把“能否通过Black检查”设成了CI的第一道门槛格式不对连测试都不会跑。这让整个团队的代码风格保持在同一个频道上再也没有人因为格式问题在我的评审里留言了。2. Black的核心工作逻辑它怎么“收拾”了你的代码2.1 从AST到格式化Black的内部处理流程很多人以为Black就是个换行和加空格的工具但它的底层机制比这复杂得多。Black会先把Python源码解析成一棵抽象语法树在这个过程中它会丢掉源码中原本的空白和注释位置只保留代码的“骨架”。随后它会把语法树重新序列化结合原始的空白信息来重新生成代码文本。这个过程之所以重要是因为Black不是靠正则表达式去机械替换文本而是真正理解了代码的结构。比如它知道一个if语句下面哪些行属于同一个代码块知道一个函数签名在哪个位置可以断行知道一个列表推导式什么时候适合压缩成一行。有了这层结构理解它处理复杂代码时才能既保持语法正确又保证风格统一。从具体实现来看Black把代码按括号的嵌套层次拆成很多“叶节点”和“容器”它给每一层括号组分配不同的换行策略优先保证每一行不超过设定的行长。这个算法有点类似排版引擎会主动寻找最合适的断点而不是从前往后硬切。2.2 magic trailing comma那个改变布局的“魔法逗号”Black的规则里有一个特别有意思的细节叫魔法逗号。假如你在函数调用或列表里写了这样一段result some_function( arg1, arg2, arg3 )注意最后一个参数arg3后面没有逗号。此时Black可能会因为一行放得下而把整个调用折叠成一行result some_function(arg1, arg2, arg3)但如果你在arg3后面加一个逗号Black就认为你是刻意想保持多行布局它会保留展开格式result some_function( arg1, arg2, arg3, )这个设计非常贴心。大家写代码时经常想保留“我故意折行了”的意图魔法逗号就是一种让意图可表达的方式。你在最后一个元素后面加逗号Black就会尊重你的多行布局不加逗号它就自己判断是否合并。这正是Black在“固执”之外的“灵活面”。2.3 引号、空行与注释Black的边界感Black对字符串引号有自己的偏好默认会把所有普通字符串统一成双引号但这有两个豁免条件。字符串内部已经包含了双引号、并且单引号不会导致转义时它会保留单引号。比如hello会被改成hello但he said hi这种字符串就保持单引号因为写成双引号需要转义成he said \hi\既不美观也难读。这个逻辑很符合“少做无用功”的原则。空行方面Black规定模块级别的顶级定义之间最多保留两个空行类内部的方法之间保留一个空行方法内部的逻辑块之间只保留一个空行。它会主动删除多余的空行也会在必要的位置补上缺失的空行。注释内容本身Black不会改动但它会随着注释归属的代码块一起移动确保格式化后注释仍然留在正确的位置。这里有一条边界线要说明Black只做代码格式的调整不会改变代码里字符串的内容不会重排字典的迭代顺序更不会改变任何业务逻辑。它是个纯格式工具不是代码优化器。2.4 不会碰的区域fmt: off和fmt: on虽然Black很“固执”但它还是给你留了一扇后门也就是# fmt: off和# fmt: on两个注释标记。在这两个标记之间的代码Black不会做任何改动。这个功能在少数场景下非常有用比如你要故意做一个对齐得很精美的表格型字典或者是一段依赖特定排版的元数据又或者是一段自动生成的代码不希望Black反复重置它的格式。# fmt: off config { key1 : value1, key2 : 123, key3 : True, } # fmt: on不过要留意# fmt: off区域里的代码会从此游离于整个项目的格式体系之外。如果把太长的代码块放进这个区域后续的维护者可能很难处理这种不一致。我的经验是能不用就不用只有当你确确实实有排版样式需要保留时才使用。2.5 关于Black的决定论和风格争论Black最被人诟病的一点是它“太武断”比如它默认的行长是88字符而不是PEP8建议的79。它喜欢在二元运算符的行尾拆行而PEP8推荐在运算符开头拆行。这导致它在刚发布时引发了不少争议。但绝大多数团队最终会发现与其为了哪种风格更好争论不休不如接受一种统一风格让所有人都能高效协作。风格本身没有绝对的对错一致性才是真正的价值。我自己的感受是Black带来的可预测性远远大于那些微小的风格偏好损失。写完代码按下保存结果永远是同一个样子这种确定性在团队项目里是无价的。3. 上手实操从Python环境安装到编辑器无缝集成3.1 安装Black与版本选择安装Black非常简单用pip即可pip install black如果你的项目使用了Poetry或Pipenv可以通过对应的依赖管理工具把Black加到开发依赖里。比如在pyproject.toml中[tool.black] line-length 88 target-version [py310]关于版本我强烈建议在项目里锁定一个固定的Black版本。因为不同版本之间的格式化规则细节可能会有细微差异如果团队里有人用的是23.x有人用的是24.x同一个文件在不同人机器上格式化出来的结果就可能不完全一致。最稳妥的做法是用requirements-dev.txt或pre-commit配置固定版本并把Black纳入统一的开发环境。3.2 命令行基础用法格式化单个文件和目录Black的命令行用法非常直接。格式化一个文件black example.py格式化整个目录black my_project/也可以传入多个文件或目录它会递归处理所有.py文件。这里有个很实用的参数是--check它不会真的改写文件而是检查文件是否符合Black的格式标准不符合就返回非零退出码。这个参数在CI里非常常用。还有一个参数--diff它会打印出如果格式化后文件会发生哪些变化方便你在执行之前先看一遍改动。black --check --diff my_project/这段命令是我每次在大型项目上准备启用Black时的第一道操作——先看一眼差异心里有数再动手。3.3 在VS Code和PyCharm里配置自动格式化如果只是偶尔在命令行里跑一下Black那体验还不够顺滑。真正舒服的用法是让它在保存文件时自动执行。对于VS Code用户可以先安装Python扩展然后在设置里指定Black为格式化器{ python.formatting.provider: black, editor.formatOnSave: true, editor.formatOnPaste: false }新版VS Code的Python扩展还支持基于pyproject.toml的配置只要你安装了Black选择之后保存文件就能看到代码被重排了。PyCharm用户配置稍微麻烦一点主要有两种方式。最简单的一种是通过插件市场安装BlackConnect插件然后在Settings里配置路径另一种是配置External Tools在菜单里手动调用Black。我自己的习惯是设置一个快捷键在写完一段代码后随时触发格式化而不是完全依赖保存触发因为有些临时文件我不想动格式。3.4 接入pre-commit钩子让格式化成为默认动作如果你在一个团队工作我更推荐用pre-commit来做格式化的执行者。pre-commit是一个Git钩子管理工具你可以在项目根目录放一个.pre-commit-config.yaml里面声明要执行的检查工具其中就包括Black。这样每次git commit的时候它会自动对暂存区里的Python文件跑一遍Black如果格式不符合它会帮你改好并让提交失败提示你重新add之后再次提交。一个基础配置长这样repos: - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black language_version: python3.11把格式化放在提交前执行的好处是它把“格式不合格”这个判断从人的评审清单里移除了机器会在提交前就把关。虽然第一次使用时可能会觉得“怎么老是提交失败”但适应之后你会爱死这个机制因为代码库的整洁度有了制度保障。4. 参数与配置真实团队中如何“调教”Black4.1 line-length88字符从哪来该不该改Black最基础的参数是行长度默认值是88。这个数字比PEP8建议的79多了9个字符用意是给现代宽屏显示器留出更多空间同时又不至于让代码过于拥挤。88并不是灵机一动拍出来的数字它是在多个知名项目里实践后得出的一个相对平衡的折中值。很多团队拿到Black后第一件事就是想改这个值。如果你问我意见我的建议是尽量别改。因为修改行长会直接影响所有的换行决策改长会让代码更密改短会产生更多的折行都会削弱Black“开箱即用”的意义。但如果你确实有特殊需求比如接手了一个已经习惯120字符行长的老项目可以在配置里显式声明[tool.black] line-length 120要留意的是改动行长后原本格式化好的代码会发生变化所以最好选择一个项目空闲时间一次性处理并且让所有开发者同步更新配置。4.2 目标版本与skip系列参数target-version参数用来告诉Black你的代码要兼容哪个Python版本比如py37、py39、py310、py311。这个参数会影响Black对某些语法换行的处理方式因为它在某些情况下需要根据目标版本决定采用哪种安全的格式化策略。如果你的项目最低支持Python 3.9就写明[tool.black] target-version [py39]另有几个skip参数也值得知道。--skip-string-normalization会禁用字符串引号的统一也就是不会再强行把单引号改成双引号--skip-magic-trailing-comma会禁用前面提到的魔法逗号特性。大多数情况下我不建议开这两个skip它们是Black少数场景下的逃生门不是常规选项。4.3 格式化工具组合Black、isort与Ruff的边界划分在实际工作流里Black经常和isort搭配使用。isort负责处理导入语句的排序Black负责其余代码的排版。这里有个常见的坑isort默认的行长是79而Black是88两者对导入行的断行判断不一致直接一起用会产生冲突。解决办法是让isort使用Black的配置档[tool.isort] profile black line_length 88这样isort就会按照与Black一致的规则来处理导入语句。近年来Ruff也非常流行Ruff自带一个formatter风格与Black基本兼容但如果你在用Black我的建议是让Ruff负责lint和import排序让Black继续负责格式化各司其职避免重复劳动。4.4 性能问题与增量格式化思路有些大的代码库跑一次Black需要花不少时间这会让开发者有点嫌弃。这其实是合理的事情因为Black要先解析语法树再重写文件文件越多自然越慢。但从实际数据来看对于大多数中小型项目Black的处理速度是可以接受的通常在每秒处理几十到上百个文件之间。如果你碰到速度瓶颈有几个思路可以尝试。一是利用--quiet参数减少输出IO二是尽量只在commit前格式化暂存区而不是每次都扫全量代码三是在CI里使用缓存pre-commit自带缓存机制改动过的文件才会被重新格式化。还有一个小技巧是black --check和black相比不会写出文件在CI里靠它做校验就够了不需要在CI里也格式化一遍磁盘上的文件。5. 把存量项目交给Black迁移中的坑与解法5.1 先跑一次--diff看清楚影响面真正把Black引入一个大项目时最忌惮的不是它效果不好而是改动范围太大大到难以审查。让我印象很深的一次迁移是在一个有一百多个Python文件的旧系统上我先是小心翼翼地跑了一次black --check --diff .输出的内容刷了整整几页终端。那一刻我意识到如果直接全局执行格式化这个commit的diff会大到几乎不可评审。所以我改变了策略按模块分批迁移。先从独立的工具模块开始再逐步扩展到核心业务代码。这种渐进式的思路让每次格式化的改动都控制在一个相对可审查的规模内。5.2 历史commit与git blame变乱怎么办存量项目一次性格式化后最让团队头疼的问题就是git blame失去了意义。某一行代码是半年前还是三年前写的会被同一个巨大的“格式化提交”盖住。这个问题不是Black独有的任何大规模重构都有类似影响。解法也不是没有。比较推荐的做法是在项目里创建一个.git-blame-ignore-revs文件把那次格式化提交的commit hash写进去然后配置Git在显示blame的时候忽略这个提交git blame --ignore-revs-file .git-blame-ignore-revs你可以通过git config blame.ignoreRevsFile .git-blame-ignore-revs把这个设置固化下来。Git会尽量追溯被忽略提交之前的作者信息虽然不能百分之百还原但能很大程度上缓解历史归属混乱的问题。5.3 必须保留原格式的区域用注释明确标注在迁移过程中我遇到过一些特殊代码比如带有表格特征的数据结构、模型定义、自动生成的ORM映射这些代码一旦被Black改动可读性反而变差。对于这些区域我选择用# fmt: off和# fmt: on把它们圈起来。这是对格式化的主动豁免也是团队在“统一性”和“特殊性”之间找到的平衡点。当然使用这个后门需要自律。我会在代码评审时要求开发者说明为什么需要豁免避免有人拿它当偷懒的借口。如果你发现一个项目里# fmt: off用得过多那多半是Black的配置和这个项目的实际情况匹配度不高值得回头看看行长和括号展开策略是否有问题。5.4 渐进迁移的具体操作和尝试思路如果你想在存量项目中快速享受Black的好处又不想一次性吃下巨大的改动可以参考一种“边缘开始”的策略。先找那些没人维护的工具脚本、测试夹具、配置文件跑一遍格式化让团队看到变化然后挑一个核心模块在代码评审中专门审查这次格式化改动确认没有隐藏问题最后再统一执行全量格式化并在commit message里明确标记为“style: apply black formatting”。在迁移时还可以借助新版Black的--line-ranges参数做行级增量格式化。这个参数允许你传入一个起始行号和结束行号Black只处理指定行区间内的格式化不触碰文件其余部分。这样即使你只想格式化某个改动过的函数也不用担心整个文件被动过。不过这个参数是相对新的能力如果你的Black版本比较旧就升级到最新版再尝试。6. 让Black成为团队纪律CI/CD集成与持续格式化6.1 在GitHub Actions里用最少的配置实现Black检查命令行用得好只能保证你个人的代码是干净的但团队协作需要一个强制机制。把Black检查放进CI是最直接的方法。下面是一个非常精简的GitHub Actions配置它的作用是在每次push和PR时检查Python代码是否满足Black规范name: lint on: [push, pull_request] jobs: black: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - run: pip install black - run: black --check --diff .这段配置的逻辑很直白先在干净的CI环境里装好Black然后对整个项目跑一遍--check。如果有任何一个文件不符合规范命令退出码非零整个job就失败PR就会在合并前被拦截。这个流程没有任何人工干预空间但正因为如此它才是可靠的。6.2 提高CI体验让报错信息友好一点black --check --diff的输出虽然能看出哪里不对但对不熟悉Black详情的队友来说可能会有点困惑。为了让CI失败信息更友好可以借助一些行动封装比如使用社区维护的github-action-black它能够把Black的diff直接以注释形式贴到PR页面开发者在网页上就能看到自己的哪一行不符合规范。另一种做法是在CI里不只检查还自动提交格式化结果。例如在PR分支上运行一个job如果代码格式不合规就用git commit --set-upstream把Black修正后的文件提交回去。这样开发者提交之后什么都不用管如果格式有问题机器人会自动帮他收拾。这种做法对新手最友好但要注意给机器人配置写权限并且避免机器人反复提交造成循环。我个人的建议是最好还是用check模式配合友好的错误信息引导开发者自己执行一遍Black理解格式化逻辑比被“代劳”更有价值。6.3 更进一步用Black生态改善代码库健康度Black的生态这几年越做越完善。它官方提供了Jupyter Notebook支持安装时多装一个black[jupyter]就能直接格式化.ipynb里的代码单元格。对于常年和Notebook打交道的分析团队来说这是很大的便利。另外有一个叫blackd的小工具是一个Black后台服务。你可以通过HTTP请求把代码发给它它返回格式化后的结果。编辑器插件可以接入这个服务实现“边打字边格式化”避免了每次保存时启动Python解释器的开销。虽然是锦上添花但对追求极致快捷键体验的人来说很值得试试。在我处理过的项目里引入Black之后真正意义上的格式争论几乎消失了。格式化变成了一个后台行为而不是需要人做的事。我的最终体会是与其制定并维护一套冗长的代码风格文档不如选一个格式化工具然后在一个固定的时间点让整个代码库被重新统一。刚开始几天大家可能不习惯但坚持一个月后回头再看旧代码你会不自觉地想给它们也跑一遍Black。那种把代码库“擦干净”的感觉确实是会让人上瘾的。
返回列表