ARTICLE DETAIL

资讯详情

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

Python代码格式化神器Black:强制统一风格,让团队协作更高效

Python代码格式化神器Black:强制统一风格,让团队协作更高效 Black 这个工具说它是 Python 圈子里最“霸道”的格式化器一点都不过分——它几乎不给你任何配置选项运行起来会“强行”把你的代码改成它认为最标准的样式也因此被很多人戏称为“不再妥协的代码格式化器”。我第一次跑完 Black 之后看着满屏的代码风格变化心里其实有点抗拒觉得它管得太宽了但用了一周之后就真香了现在写 Python 项目基本上离不开它。这篇内容围绕 Black 的实际使用展开重点讲清楚它到底是什么、为什么要用、怎么在项目里落地、以及实际使用中会踩到哪些坑。适合正准备接触代码格式化、或者已经在团队里被代码风格问题折磨过的 Python 开发者参考无论你用 IDE 开发还是只写脚本Black 都能直接帮你把“风格统一”这四个字落地。1. 为什么代码格式化会成为团队的痛点而 Black 是那个解药1.1 每个人都有自己的“审美”代码风格就是吵架的源头我见过太多项目因为代码风格问题导致代码评审变成“辩论赛”。有人喜欢用四个空格缩进有人偏爱两个空格有人习惯字符串用单引号有人坚持双引号有人写函数参数喜欢一个占一行有人觉得那样浪费屏幕空间。这些细节单独看都不致命但混在一起就会让代码变得非常难读。尤其当团队里来了新人或者项目开源出去之后收到各种风格的贡献代码你很快会发现大量精力不是花在“写功能”上而是浪费在“对齐格式”上。写代码的目的是让机器执行但读代码的始终是人。统一的风格能让阅读者把注意力集中在逻辑本身而不是被无关紧要的格式差异反复打断。这就是代码格式化工具存在的根本意义把风格问题从“人的讨论”变成“机器的约定”。1.2 手工统一风格不可持续自动化才是唯一的出路有人会说团队规范文档写清楚不就行了说实话我见过太多贴满墙的编码规范文档最终结局基本都一样新人认真执行了两周老员工忙起来根本顾不上到了三个月之后规范就成了一纸空文。原因很简单靠人自觉去遵守规范本质上就是在消耗每个人的自律额度而人的自律额度是有限资源。我的建议是在项目里引入自动化格式化工具让机器在每次保存代码的时候自动帮你整理。这个逻辑就像家里请了个保洁阿姨你不需要每天操心垃圾有没有分类只需要约定好阿姨什么时候来房间就一直是整洁的状态。放到代码里来说Black 就是这个保洁阿姨而且是那种雷厉风行、说一不二的类型。1.3 Black 的设计哲学不给你选择反而解放了你在用 Black 之前我试过 YAPF 和 autopep8这两个工具都是“尽可能地让你配置”于是我花了大量时间翻文档、调参数、对比风格最后发现配置本身变成了一种负担。而 Black 的理念完全相反它几乎不接受个性化配置所有的格式化规则都是预设好的你只有两三个参数可以调整。一开始我觉得这是缺点但用久了才明白这恰恰是它最聪明的地方。当格式化规则由个人决定时每个人都会倾向于“自己的偏好才是对的”当规则完全由工具决定时大家反而能心平气和地接受。Black 用“独裁”换来了团队的“和平”代码风格问题从“我们该用哪种风格”变成了“我们该不该用 Black”而后者往往很快就能达成共识。Black 内部的格式规则文档很详细比如字符串引号的处理、逗号的处理、括号换行的规则都是项目维护者基于大量真实代码库的统计和实验得出的结果。你不需要理解每一条规则的动机只需要相信一件事它输出的代码风格是一致的、可预测的、并且比大多数手写代码在多数场景下都更易读。2. Black 的安装与核心参数先把工具跑起来2.1 环境准备安装 Black 的三种方式Black 是一个标准的 Python 包安装方式非常直接。如果你的环境里已经装好了 Python 和 pip那么一条命令就能装完pip install black我个人的习惯是把 Black 装在项目的虚拟环境里而不是全局环境。原因有两个第一项目虚拟环境里装的东西会记录在依赖文件里别人 clone 项目之后能复现同样的环境第二避免不同项目对 Black 版本的要求冲突。如果你用的是 pipenv 或者 poetry就用自己的包管理器把 black 加到开发依赖里。还有一种更省心的方式是装black[jupyter]这个版本它会附带 Jupyter Notebook 的支持。我们平时用 Jupyter 写分析代码的时候Notebook 的单元格里那叫一个随心所欲Black 可以直接格式化.ipynb文件中的代码单元格这一点对做数据分析和机器学习相关工作的朋友特别友好。安装完之后验证一下版本black --version如果能看到类似black, 24.4.2 (compiled with CPython 3.12.0)这样的输出说明安装成功。2.2 最常用的两个命令直接格式化与检查模式Black 最基本的用法是直接指定要格式化的文件或目录# 格式化单个文件 black my_script.py # 格式化整个目录递归处理所有 .py 文件 black . # 格式化指定目录下的所有文件 black src/上面的命令会直接改写目标文件。如果你只是想看看哪些文件需要格式化、但不想立即修改可以用--check模式black --check src/这种模式只检查不修改输出结果会列出哪些文件需要格式化。CI 环境里经常用到这个模式一旦有人提交了未格式化的代码流水线就会挂掉通过这种方式强制团队提交前先跑格式化。我在本地跑检查的时候还会加上--diff用来查看具体的差异内容black --check --diff src/输出会显示格式化前和格式化后的区别这个参数在审视 Black 的改动时非常好用。2.3 用表格梳理 Black 的常用配置参数虽然 Black 号称“不妥协”但它还是留了几个关键的调节旋钮。用表格来展示这些参数会更直观参数作用默认值我的建议--line-length设置单行最大长度88保持默认除非团队有约定--skip-magic-trailing-comma关闭魔术尾逗号功能不启用保持默认开启--target-version指定目标 Python 版本py38按项目实际版本设置--extend-exclude追加排除目录/文件空排除构建目录和虚拟环境--force-exclude强制排除目录/文件空项目根目录固定排除路径--quiet静默模式减少输出不启用CI 脚本里用先说行长度。PEP8 推荐的是 79 个字符Black 把默认值设成了 88很多人第一次看到会问为什么不是 80。Black 的作者拿大规模代码库做过实验23 万行代码里超过 79 字符的行大约占 8%超过 88 字符的行只剩 3%所以在保持可读性的前提下88 能显著减少不必要的换行。我个人的体验是写业务代码时行长度设为 88 很舒服但在写深度学习模型那种带超长参数列表的代码时会频繁触发换行这时候可以适当调大比如 100 或 120不过团队内部必须统一。--target-version这个参数决定 Black 根据哪个 Python 版本判断语法兼容性。比如你的项目最低支持 Python 3.9那么--target-version py39会让 Black 在格式化时避免使用 3.10 之后才引入的语法特性。2.4 使用 pyproject.toml 固化配置团队直接共享直接在命令行里敲参数有个问题每个人每次执行都可能记错参数。我习惯把配置写到项目的pyproject.toml文件里Black 会自动读取其中的[tool.black]段[tool.black] line-length 88 target-version [py39, py310] include \.pyi?$ extend-exclude /(build|dist|venv|\.env)/ 这段配置的意思是行长度保持 88目标版本是 Python 3.9 和 3.10只处理.py和.pyi文件排除build、dist、venv、.env这些目录。这样团队里任何人运行black .时用的都是同一套规则不用再解释“你的 Black 为什么跟我的格式化结果不一样”这种问题。3. Black 的格式化规则到底怎么运作实操中会有哪些改动3.1 从一段示例代码看 Black 的改动逻辑我拿一段真实业务代码做示范格式化之前的代码长这样def calculate_total_price(unit_price, quantity, discount0, tax_rate0.1): raw_totalunit_price*quantity total_discountraw_total*discount tax_amount(raw_total-total_discount)*tax_rate final_priceraw_total-total_discounttax_amount return final_price这段代码能跑但有几个典型问题赋值操作符两边没有空格、函数参数挤在一行里过长、表达式堆积缺少呼吸感。Black 格式化之后的代码是这样def calculate_total_price( unit_price, quantity, discount0, tax_rate0.1 ): raw_total unit_price * quantity total_discount raw_total * discount tax_amount (raw_total - total_discount) * tax_rate final_price raw_total - total_discount tax_amount return final_price注意到几个关键变化。第一赋值符号周围补上了空格这是最基础的易读性改进。第二函数定义因为参数行超过 88 字符被拆成了多行且每个参数占一行。第三原来挤在一行的表达式被拆开现在每个计算步骤都清晰可见。纯粹用代码评审的方式去逐条提意见大概要写一整篇评论但 Black 一键就处理完了。3.2 字符串引号统一与表达式括号拆分Python 世界的引号流派之争Black 选择站队但不彻底。它默认会把所有的普通字符串改为双引号但是有个前提如果字符串内容本身就包含双引号改用双引号就需要转义Black 会保留单引号来避免引入多余的反斜杠。举个具体例子# 修改前 message 他说今天天气不错 # Black 保留单引号因为改成双引号会产生转义反过来如果字符串里只有单引号Black 会把它统一成双引号。这套规则的目的很简单大多数情况下统一到双引号个别场景为了避免转义而保留单引号最终读起来都自然。另一个经常被讨论的改动是括号内表达式的拆分。Black 遵循一个核心原则当一行内容超过行长度限制时它会选择最合理的换行点通常是运算符之后# 修改前 result very_long_function_name(argument_one, argument_two) another_function(argument_three) - some_value # Black 格式化后 result ( very_long_function_name(argument_one, argument_two) another_function(argument_three) - some_value )请注意 Black 的处理方式外面包了一层括号把整个长表达式括起来然后在运算符处换行运算符放在行首。这样做的目的是让每一行在视觉上对齐读起来就像一段竖排的算式逻辑层次一目了然。很多人第一次看到这种风格会觉得不习惯但多看几次就会发现Debug 的时候真的很舒服。3.3 魔术尾逗号一个看起来不起眼但很实用的魔法Black 有一个叫“魔术尾逗号”的机制理解之后你会发现它相当聪明。当你在一个多行结构比如列表、字典、函数调用末尾加了逗号时Black 会把每个元素拆成一行。比如# 原始代码 items [apple, banana, orange, grape]如果这几项排在一起超过行长度Black 会自动拆行。但如果你手动在最后一个元素后面加上逗号Black 就认为“你希望永远保持这种多行结构”即使以后删掉一些元素导致行长度缩回去了它也不会把多行结构合并回去。这个特性在实际工作流里非常有用。最常见的场景是给列表动态加元素比如一个配置项的列表今天有三个值明天可能加第五个、第六个。如果让它一直以每行一列的形式展示git diff 的时候每一行都清清楚楚评审会轻松很多。我第一次体会到这个功能的妙处是在处理一个枚举列表时每次新增枚举值都能在 diff 里只看到一行新增而不是整块列表重排。3.4 格式化时的安全机制# fmt: off 与 # fmt: on再好的工具也有不适合发挥的场景。比如某些算法代码为了性能刻意把多条语句写在一行紧凑排列或者某些自动生成的配置文件格式有特殊要求Black 如果真的把它们拆开反而会破坏原有逻辑。Black 提供了一个非常直接的“逃生舱”用# fmt: off和# fmt: on把不想被格式化的代码包起来# fmt: off matrix [ [1, 2, 3], [4, 5, 6], ] # fmt: on在这对标记之间的代码Black 会完全跳过不管格式多乱都不管。我通常在两种场景下使用这个特性一行内嵌的 SQL 字符串、以及为了保持声明式外观的序列化结构。不过要注意这种东西尽量少用用多了等于在代码里开了一堆“格式豁免区”会一点点侵蚀统一的风格基础。4. 把 Black 接入你的工作流编辑器、pre-commit 与 CI4.1 VS Code 设置保存文件时自动格式化把 Black 接进 VS Code 的体验非常顺滑。现在新版 VS Code 里Python 扩展已经支持直接指定格式化工具为 Black。如果在settings.json里做了如下配置那么每次保存.py文件时Black 都会自动跑一遍{ [python]: { editor.defaultFormatter: ms-python.black-formatter, editor.formatOnSave: true, editor.codeActionsOnSave: { source.organizeImports: explicit } } }上面代码里用了微软官方的black-formatter扩展这个扩展会在本地环境里调用 black 命令。设置完成之后你几乎感觉不到它的存在代码写完之后顺手按个保存格式就自动整齐了。我习惯把editor.formatOnSave设为true有人会担心保存时自动修改代码导致 diff 范围不可控我的经验是坚持“先格式化再写提交”的原则这样 diff 永远是干净的不会出现“顺手格式化了一大片无关代码”的尴尬情况。4.2 PyCharm 的插件与外部工具配置PyCharm 用户有两种接入方式。最简单的是直接在插件市场搜索 Black安装插件之后选择 Black 作为默认的格式化工具。另一种方式是配置外部工具在 Settings 的 Tools 里新建一个 External ToolProgram 填 python 解释器的绝对路径Arguments 填-m black $FilePath$Working directory 填$ProjectFileDir$然后绑定一个快捷键比如 CtrlAltB。这种方式的好处是不依赖插件市场灵活性更高。PyCharm 里有一个小坑内置的 Reformat Code 功能默认用的是它自己的格式化引擎跟 Black 的规则不完全一致。所以用外部工具方式配置后记得把默认的格式化快捷键覆盖成调用 Black。否则你按了 AltEnter 或者 CtrlAltL出来的风格还是 PyCharm 默认的Black 的规则就不会生效。我团队里有同事就是把这两个搞混了折腾了大半天才发现。4.3 pre-commit提交之前就把不规范代码挡住对于团队项目或者开源项目我更推荐用 pre-commit 在 git 提交阶段做拦截。pre-commit 是一个用配置文件管理多个代码检查工具的程序你只需要在项目根目录写一个.pre-commit-config.yamlrepos: - repo: https://github.com/psf/black rev: 24.4.2 hooks: - id: black language_version: python3项目成员第一次使用时执行一次pre-commit install之后每次git commit时Black 都会先检查暂存区的 Python 文件如果有格式问题就直接修改文件内容此时提交会被中断你 review 一下改动后重新git add再git commit就好。这里有一个需要注意的点pre-commit 修改的是工作区的文件修改完之后必须重新git add否则 commit 还是会失败。很多刚接触 pre-commit 的新人容易在这一步卡住误以为工具跟 git 冲突了。其实这就是一个正常的流程设计阻断提交、让你检查改动、再重新提交。从流程设计上杜绝了未格式化代码进入版本库的可能。4.4 CI 管道里加上格式检查守住合并前最后一道关团队协作时光靠本地 pre-commit 还不够因为在某些场景下成员可能绕过钩子。在 CI 流水线里加一个 job 专门做格式检查是最稳妥的做法。GitHub Actions 里可以这样写jobs: format-check: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - run: pip install black24.4.2 - run: black --check --diff .这个 job 只需要几十秒就能跑完成本极低但能把所有“忘记格式化”的代码挡在合并之前。有了 CI 检查之后格式问题基本不会再进入到评审环节代码评审的焦点就能完全集中在设计逻辑和功能实现上这比任何编码规范文档都有效。5. 常见问题与实战避坑指南5.1 Black 把我的代码都改了diff 变得太大怎么办这是很多人第一次跑 Black 之后最真实的反应。如果你在一个成熟的老项目里引入 Black第一次格式化必然会产生一个巨大的 diff甚至波及全仓库的每个文件。这种情况确实会让 reviewers 头疼。我的处理方式是分两步走。第一步创建一个单独的提交只包含 Black 格式化的结果标题写清楚“chore: apply black formatting to entire codebase”并且跟团队成员确认好这个提交不 review 代码逻辑改动大家只确认格式化结果没问题就放行。第二步以后所有功能提交都建立在格式化后的基线上diff 就干净了。首次格式化确实有阵痛期但痛一次之后换来的是长期的舒爽这笔买卖是划算的。5.2 Black 跟 flake8 或 pylint 同时使用时的规则冲突Black 只管格式化不管代码质量检查Lint实际项目里通常会同时引入 flake8 或者 pylint。这两类工具的规则集有重叠也有冲突最常见的一个矛盾点在于行长度。Black 默认按 88 字符折行而 flake8 的 E501 默认检查 79 字符两者必然打架。解决方案有两个要么在 flake8 配置里把 max-line-length 改为 88要么让 Black 的行长度改为 79。大部分人选择前者因为 Black 的 88 字符在统计上更加合理。pylint 那边有一个更隐蔽的冲突项Black 拆行后常常产生“trailing comma”而某些 pylint 规则对此有意见这种情况需要在 pylint 的 disable 列表里加上对应规则号。我的固定套路是把所有静态检查工具的 max-line-length 跟 Black 对齐三角形的三个顶点格式化工具、风格检查器、程序员才能达成一致。5.3 Black 格式化后git blame全乱了历史追溯困难这是代码格式化的“原罪”无论用哪个工具都存在。git blame 会显示最后修改过这一行的提交如果格式化把整个文件的行都重排了git blame 就会被格式化提交“污染”后面想查某行代码是哪个功能引入的会非常麻烦。我的解决方案是维护一个统一的格式化提交作为基准要求所有历史追溯都在这个提交之后进行。如果确实需要追溯格式化之前的行可以在git log里跳过格式化提交查看历史git blame 文件 --ignore-rev 格式化提交的hash。这个命令会忽略指定的提交对 blame 的影响能追到格式化之前的提交信息。更细的做法是把格式化提交的 hash 写到.git-blame-ignore-revs文件里在项目级配置中声明git config blame.ignoreRevsFile .git-blame-ignore-revs这个方法我用了很久每次团队开新项目我都会提前把这一套配好这样格式化带来的历史污染基本可控。5.4 我想保留自己的代码风格Black 却非要用它的规则这个问题本质上是心态问题。我见过不少开发者对着 Black 的输出气不打一处来觉得自己精心调整的对齐全被毁了。平心而论Black 的默认格式化结果在大多数情况下的可读性确实很好但偶尔也有一些让个人不适的风格比如它喜欢把所有能用多行表示的列表全部多行展示即便列表只有两项且放得下。应对这种情况的建议有两条。第一在该用# fmt: off的地方果断用不要觉得“用了逃逸舱就不专业了”反而把它当一种精确定制的手段。第二如果 Black 的某个具体行为实在难以接受可以到项目仓库的 issue 区反馈Black 的维护团队会收集真实使用者的意见有些规则确实随着版本迭代在变动。但记住一个前提一旦团队决定用 Black任何个人对格式化结果的偏好都应该让位于统一的工具输出这种“牺牲个人偏好换取整体一致”的思维方式恰恰是专业协作的核心。5.5 Python 低版本环境跑不了新版 BlackBlack 的新版本对 Python 版本是有要求的。比如说 Black 24.x 本身要求 Python 3.8 以上才能运行如果你项目的运行环境还是 Python 3.7 甚至更老可能就需要锁定一个兼容的 Black 旧版本。处理办法是查 Black 的 release notes选一个与项目 Python 版本兼容的 Black 版本并把它固定在 requirements-dev.txt 或 pre-commit 的 rev 里。别让“工具版本不一致导致格式化结果不同”这种低级问题成为团队的内耗源。5.6 处理大型目录时速度太慢怎么办Black 有一点经常被人吐槽大项目全量格式化会比较慢。我在一个接近 10 万行代码的仓库里跑过black .耗时十几秒钟。如果觉得这个时间难以接受可以做两件事第一用--quiet参数减少输出开销第二或者把 exclude 规则配置好不让 Black 去扫描本就不需要格式化的目录——只处理 src 目录比处理整个仓库快得多。真正高频的格式化场景是保存文件时触发的单文件操作那个速度永远都在毫秒级完全无感知。6. 一些番外内容Black 的版本演进与生态地位6.1 稳定版策略v22 之后 Black 宣布代码格式化风格已稳定Black 在 2022 年发布了 22.x 稳定版并且郑重宣布核心的格式化风格已经锁定之后不会再出现破坏性的格式变化。这对使用方来说非常重要意味着你不用担心升级 Black 会导致全仓库的代码格式突然再来一次大变动。像我们做基础组件维护的最怕的就是格式化工具隔几个月改一次规则然后把历史代码全部翻一遍。稳定版策略一出来团队就可以放心把 Black 锁进 CI 和 pre-commit长期使用没有后顾之忧。6.2 Black isort 的组合拳格式与 import 顺序一起搞定Black 本身不管 import 顺序它只负责代码排版。而 import 顺序其实是另一个容易乱的点标准库、第三方库、本地模块的顺序问题各团队也有不同习惯。行业里的通行做法是 Black 配合 isort 一起用isort 专门处理 import 排序和分组Black 处理其余代码风格。isort 有一个 Black 兼容模式在配置里加上profile blackisort 就会使用与 Black 风格匹配的折行和引号规则。这一对组合几乎是现代 Python 项目的标配开箱即用省心得很。我自己在脚手架里同时配好这两个工具pre-commit 钩子列表里它们也是前后脚执行。6.3 处理 Jupyter Notebook黑科技加持的数据分析体验之前提到过black[jupyter]这个扩展包它对做数据分析的人非常实用。Notebook 文件通常没法用普通的black script.py直接格式化需要专门的支持。安装扩展之后直接运行black notebook.ipynbBlack 就会格式化这个 notebook 里所有代码单元格。我做数据分析时的习惯是每写完一个单元格立刻跑一遍快捷键这样既保持了 notebook 整洁又不会破坏已有的输出结果。Black 对 ipynb 的处理只改代码单元格不动输出的 Markdown 文本等内容安全性是有保障的。7. 写在最后的实践经验从我这几年的实际体会来看用 Black 这件事最大的价值不在于代码美观——美观只是表面的结果更核心的价值是解放了团队协作中不必要的注意力消耗。每个开发者的大脑资源本来就很宝贵与其花在讨论缩进和引号上不如全部投入到业务逻辑、架构设计和代码质量上。格式化这种机械的事情本来就该交给机械的工具人类应该做更有创造性的事。如果你是从零开始一个新项目我的建议是第一天就把 Black、pre-commit、CI 检查这一整套东西配好新项目会从一开始就保持统一的风格。如果你是在老项目里引入 Black那就做好首次大提交的沟通一次格式化提交作为历史基线整体成本完全可控。以后你大概率会遇到各种“Black 好霸道”的吐槽但你会发现真正用过一段时间之后几乎没有人愿意回到没有自动格式化的日子。
返回列表