ARTICLE DETAIL

资讯详情

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

Python代码格式化工具Black:从原理到团队落地的完整指南

Python代码格式化工具Black:从原理到团队落地的完整指南 1. 为什么格式化这件事值得较真先说个我自己的经历。几年前参与过一个持续迭代了快四年的项目代码库规模不小团队前后换过好几拨人。每次打开一个不熟悉的模块最先让我头疼的往往不是业务逻辑有多绕而是缩进风格五花八门、引号单双混用、逗号有的加有的不加、函数参数有时候三行有时候挤在一行。你问这些影响功能吗不影响。但每次 code review 的时候总有人因为“这里是不是该换行”“那里是不是多了个空格”吵起来真正的逻辑问题反而被淹没在这些噪音里。后来我们引入了 Black情况一下就变了。Black 是一个号称“不妥协”的 Python 代码格式化工具你不需要配置复杂的规则它直接给出一种格式化风格并强制执行。代码入库之前用 Black 跑一遍所有人的代码看起来都像同一个人写的。从此 review 的焦点终于回到了“这个逻辑对不对”而不是“这个空格对不对”。这篇文章我会从原理讲起覆盖安装、命令行用法、IDE 集成、团队协作落地、常见误区和一些我踩过的坑。适合完全没有接触过代码格式化工具的新手也适合正在犹豫要不要在团队里推行 Black 的开发者。全文内容都以我的实际操作经验为基础命令和配置都是验证过的你可以直接照着抄。2. Black 是怎么把代码“重新排版”的Black 不是一个简单的文本替换工具它内部的工作方式比很多人想象的要有意思得多。2.1 先解析成 AST再重新生成代码Black 会把你的源码解析成一棵抽象语法树AST然后完全忽略你原本的排版方式按照它自己的规则把这棵语法树重新“打印”成代码。这带来一个很关键的特性它不关心你的原始格式有多乱它只按自己的一套规则输出。这个过程类似你抄写一篇文章——但你没看排版只看文字内容然后按自己的排版规范重新打了一遍。所以不管原作者怎么换行、怎么缩进、怎么加空格只要语法语义不变Black 输出结果的规范化程度是一样的。AST 解析的好处是Black 能结构性地理解代码。它能判断出一个括号里是参数列表、函数定义还是表达式从而决定换行策略和缩进级别。这是普通正则替换完全做不到的。2.2 默认规则的几个硬性偏好Black 的默认规则不多但每一条都很有代表性我挑几个最影响日常写码的说行宽默认 88 字符不是 PEP 8 建议的 79。这个 88 是 Black 作者权衡后的选择因为 79 在实际使用中太容易触发换行反而让代码可读性下降。88 是一个在所有主流屏幕上都能良好显示的宽度。字符串默认全部使用双引号除非原字符串里已经包含双引号或者你通过配置关闭了转换。括号内如果有元素超出单行宽度Black 会展开成垂直排列并在最后一个元素后面自动补一个逗号。这个后面细说它叫“魔法尾逗号”是 Black 风格里的灵魂规则之一。二元运算符会整体换行而不是运算符后置。旧代码里常见的a b c方式会被重新排版成运算符开头的新风格。多个空行会被压缩到最多一个模块顶层最多两个行末多余的空格会被清掉。这些规则组合起来让 Black 格式化出来的代码有一个非常明显的视觉特征结构对齐极好嵌套用统一的 4 空格缩进拆行的逻辑非常机械但一致。2.3 safe 模式和格式化前的“安全检查”Black 有一个很多人不知道的底层保护机制格式化前它会先把源码解析成 AST格式化完成后再把格式化后的代码重新解析成 AST然后比较前后两棵 AST 是否一致。如果不一致说明格式化破坏了语义Black 会拒绝输出结果并报错。这个机制默认开启对应的命令行参数是--safe你可以用--fast关闭它来提速。但我不建议日常使用--fast因为我实测过运行速度差异其实没有大到影响使用体验而--safe能在极少数边界情况下兜底。比如 Python 2 向 Python 3 过渡时代的某些语法糖、注释里的特殊编码声明AST 一致性检查都有用。3. 安装配置与三种使用姿势3.1 安装与版本选择Black 的安装很简单用 pip 就行pip install black如果你的项目用了 pipenv 或 poetry对应加参数安装即可。一个基本的习惯是把 Black 固定到一个大版本范围内它目前发版速度不算快但偶尔会有格式化规则的微调。这种微调对你没影响但会导致上次格式化过的代码升级 Black 后跑一遍又出现新的 diff。对于团队协作场景很烦所以建议在项目里锁版本。看看我能查到的常用安装命令pip install black23.3.0 # 锁定精确版本 poetry add -D black # poetry 开发依赖 pipenv install --dev black # pipenv 开发依赖3.2 命令行基础操作安装完成后最简单的运行方式black ./src这条命令会递归格式化src目录下所有 Python 文件。平时迭代中我更常用的是black --check ./src--check模式只检查不修改会列出哪些文件没有被格式化返回码非零。这个参数在 CI 里非常关键后面讲团队协作时会用到。如果要单独格式化某个文件black app.pyBlack 执行完成后会打印类似reformatted 3 files或Unchanged 2 files的信息看到reformatted就说明文件被改动了。3.3 把 Black 接到你的编辑器里命令行用熟了之后接下来必须把 Black 集成到编辑器里这才是提升日常效率的关键。在 VS Code 中安装官方 Python 扩展后可以在设置里指定{ python.formatting.provider: black, editor.formatOnSave: true }新版 VS Code Python 扩展已经改成了python.formatting.provider指向 Black 的方式。设置好之后每次CtrlS保存文件都会自动跑 Black 格式化。我在几个不同的编辑器里都试过 Black 集成——VS Code、PyCharm、Neovim结论是它们调用 Black 的方式大同小异基本就是配置一个格式化命令指向 black。但有一个额外建议把 Black 设置为默认格式化工具之后关掉编辑器的“仅格式化某部分”之类的增量格式化功能。因为 Black 是全文件格式化工具它不看你的光标位置只要保存就全量重排整个文件。增量格式化工具和它一起工作会出现“明明保存过怎么还有变更”的错乱感。另外之前热搜词里有个“idea代码格式化失效”如果你碰到 PyCharm 里 Black 格式化没效果先检查下面几项插件是否安装、Black 解释器路径是否选对了虚拟环境、有没有和 IDE 自带的格式化器冲突。这个我后面在第七部分再展开。4. 核心配置项与 pyproject.toml你可以在项目根目录创建一个pyproject.toml文件把 Black 的配置集中管理。这样所有使用这个项目的开发者、CI 系统、IDE 插件都读同一份配置避免“在我电脑上是好的”这种问题。4.1 一份可用的最小配置模板[tool.black] line-length 100 skip-string-normalization true target-version [py311] include \.pyi?$ extend-exclude /( \.git \.venv \.tox \.mypy_cache \.pytest_cache \.ruff_cache build dist )/ 这个配置里我改了两个最常用的选项行宽从默认 88 调整到了 100并关闭了字符串引号归一化也就是允许代码里保留单引号。为什么这么改因为在我们实际项目中业务代码是为内部 API 服务的单引号和双引号混用并不会造成认知负担而 88 字符对某些很长的 SQL 字符串和测试断言来说确实容易频繁换行100 是团队的折中选择。4.2 常用选项详解我把 Black 的常用选项整理成一张表方便你按需选择选项默认值作用与使用建议-l/--line-length88设置最大行宽超出则拆行。建议先按默认用一周再根据团队感受调整-S/--skip-string-normalization否跳过字符串引号归一化。团队如果对引号无执念开启可以少一些改动--skip-magic-trailing-comma否关闭魔法尾逗号功能。一般不推荐关闭这是 Black 的核心特色-t/--target-version自动检测指定 Python 版本影响语法特性判断。老项目建议显式设置--check否只检查不修改。用于 CI 和 pre-commit--diff否输出差异而不改动文件。配合 check 看差异特别方便--color自动开启彩色 diff 输出4.3 魔法尾逗号到底是干什么的我这里想单独把魔法尾逗号拎出来说因为它是理解 Black 风格的关键。假设你有一个函数调用参数很多Black 会先按单行放result calculate(a, b, c, d)一旦这一行超过了行宽就会展开成result calculate( a, b, c, d, )注意最后一个参数d后面带了一个逗号。这个逗号就是“魔法尾逗号”它告诉 Black即使这串参数后面改短了、宽度没那么紧张了也请永远保持这种展开的格式。如果你手动删掉这个逗号写回result calculate( a, b, c, d )那么 Black 下次格式化时如果发现括号内容可以放进一行就会把它重新压缩回result calculate(a, b, c, d)。这个规则的实际价值在于调试代码时非常省事想临时注释掉最后一个参数或者增删参数时不用去改上一行的末尾逗号git diff 更干净。我从实践中感受到尾逗号是 Black 对“代码可编辑性”的隐性贡献很多团队从别的格式化器切换过来后最明显的不适恰恰在这里需要几天的适应。5. 结合 Pre-commit 和 CI 实现自动化格式化这种事靠每个人自觉保存时跑一下是不够的。总有漏网的、总有没配编辑器的人。团队落地 Black 的正确姿势是双管齐下本地靠 pre-commit 拦住远端靠 CI 兜底。5.1 配置 git pre-commit hook最常见的方案是结合pre-commit框架。先在项目里装好 pre-commitpip install pre-commit pre-commit install然后在项目根目录建一个.pre-commit-config.yamlrepos: - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black这一步之后每次git commit时 pre-commit 都会先跑 Black。如果有文件需要格式化pre-commit 会拦截提交并直接把格式化结果写入文件系统。你看到失败后重新git add再git commit就可以了。这个过程我第一次用时也觉得很麻烦“为什么不能自动改完直接提交”。后来想明白了pre-commit 只负责在提交前发现不合格的代码但不替你完成提交流程。这样做的好处是日志干净每次 commit 你都能清楚看到 Black 到底改了什么。5.2 在 GitHub Actions 里跑 Black 检查本地拦截并不够。总有人跳过 pre-commit或者用了旧版 Black 本地格式化结果和 CI 的配置不一致。CI 一定要再加一道保险。下面是一个可以用的 GitHub Actions 工作流片段name: lint on: push: pull_request: jobs: black: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-pythonv4 with: python-version: 3.11 - name: Install dependencies run: | pip install black23.3.0 - name: Run black --check run: | black --check .注意两点第一CI 里 Black 必须和本地固定同一个版本否则会出现“本地格式化过了但 CI 还是报错”的情况。第二black --check .会在有未格式化文件时返回非零退出码从而让 CI job 失败。我们团队甚至加了一个步骤失败时输出black . --diff的差异让开发者在 CI 日志里直接看到需要改哪些地方。5.3 增量接入大项目的渐进策略如果你的项目是已有的大存量代码库一次性跑 Black 全量格式化会产生一个巨大的 commit破坏 git blame 历史review 起来也痛苦。这里有三种渐进策略可以选只格式化新文件和改过的文件维护一个 ignore 列表之后逐渐缩减列表范围。先格式化整个项目然后把 git blame 忽略掉配置.git-blame-ignore-revs文件把那次大规模格式化提交的 hash 写进去。按照模块逐步接入每个模块里先配置局部排除.gitignore或project-specific exclude轮到哪个模块就移除对应排除项。我第三点想展开说。Black 的exclude配置是支持正则的你可以精确控制“哪些路径不格式化”。例如[tool.black] extend-exclude /( legacy_engine tests/data )/ 这样前期只让 Black 管新代码旧模块的债务先欠着等后续重构时再逐步补上。这种做法的心理负担小很多不会因为一次大面积改代码产生抵制情绪。6. Black 与 isort、Ruff、Flake8 的协同工作Black 只管格式化它不处理 import 排序、不检查未使用的变量、不检测逻辑问题。所以通常需要和其他工具配合使用这里的搭配顺序有讲究。6.1 isort 负责 import 排序isort 是专门做 import 排序的工具。你可能会问Black 不是会格式化 import 块吗是的Black 会把 import 语句按自己的规则重排但它不做“分类排序”——比如标准库和第三方库分离、按字母排序这些是 isort 的活。isort 需要配置成与 Black 兼容的模式isort --profile black .这个profile black参数是关键它让 isort 的缩进、引号、行宽等设置向 Black 看齐避免两个工具互相打架。如果你用 pyproject.toml 统一配置[tool.isort] profile black line_length 1006.2 Ruff 可以替代其中几个工具最近两三年Ruff 因为速度快、配置简单在 Python 社区越来越流行。Ruff 本身是一个 linter formatter它内置了ruff format这个命令的格式化风格就是向 Black 兼容的。如果你是新项目可以直接用 Ruff 一把梭——用ruff format替代 Black用ruff check做 lint。但如果你已经在用 Black其实不用急着切换Black 的生态已经很成熟了。工具越多带来的调试成本也越高我个人的建议是“一个项目固定一套工具链不频繁更换”。6.3 Flake8 与 Black 的配置冲突如果你用 Flake8 做 lint需要注意两个规则冲突。E203 冲突Black 喜欢在切片操作符:两侧不加空格但 Flake8 的 E203 会报错要求在冒号后加空格。需要把 E203 从 Flake8 的忽略列表中移除。W503 冲突Black 把二元运算符放在行首而 Flake8 的 W503 规则要求运算符放在行尾。也要忽略掉。在.flake8文件中的配置长这样[flake8] ignore E203, W503 max-line-length 100这里max-line-length同样要与 Black 的line-length保持一致不然 Flake8 会对 Black 认为合理的行宽报告过长的错误。6.4 用 pre-commit 组合整套工具链把 Black、isort 和 Flake8或 Ruff放在同一个 pre-commit 文件里顺序很重要一般是repos: - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort args: [--profile, black] - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8先跑 Black 格式化再跑 isort 排 import最后 flake8 做静态检查。如果顺序反了isort 会把 Black 刚格式化好的 import 块重新排列产生无意义的 diff。7. 我在实际项目中踩过的坑7.1 魔法尾逗号带来的多行 diff前面夸了魔法尾逗号但我必须承认它也会捣乱。有一次我重构一段代码原本是这样的items fetch_items( user_id, page, )后来page参数被删掉了。我改成items fetch_items( user_id, )因为尾逗号还在Black 认为这里应该永远展开即使fetch_items(user_id,)明明可以放在一行。每次看到这种单参数却占三行的代码我起初都觉得非常丑甚至一度想让团队关闭这个功能。但后来我发现这个表现本身是一种特性你删除参数时如果保留尾逗号说明这里未来可能会加回参数如果决定不再需要展开格式删掉尾逗号Black 会立刻帮你压回单行。理解了它之后反而觉得顺手了。7.2 不同 Black 版本导致 CI 与本地结果不同这是我们团队真实踩过的坑。某位同事本地用的 Black 比较新CI 里还是旧版结果他格式化完代码推送后CI 的black --check总是提示有文件需要格式化。调试了半天才发现是新版 Black 对字典展开的行为做了细微调整。从那以后我们做了三件事将 Black 版本写死在 requirements-dev 和 CI 工作流里在 README 里注明“本仓库使用 Black 23.3.0”把 pre-commit 的 rev 也固定到对应 tag。版本不一致导致的“薛定谔的格式化”是最难排查的一种问题因为你看代码明明已经是格式化过的了但工具就是不满意。7.3 Black 和 Jupyter Notebook 文件的处理Black 对.ipynb文件的支持是有限的。旧版 Black 不会处理 Notebook 里的 cell 代码。如果你用 Jupyter Notebook 写代码又想用 Black 格式化有两个方案将 cell 里的代码复制到.py文件格式化后再粘贴回去笨但可用。使用nbqa这个工具它可以对其他命令行工具做 Notebook 适配。我建议团队约定Notebook 里的代码以.py文件为主Notebook 只保留说明和可视化结果。这样既能让 Black 完全覆盖代码审查又不会因为nbqa的转换产生额外的维护成本。7.4 不要把 Black 当成代码审查替代品Black 确实能消灭格式讨论但如果你以为引入 Black 后 code review 就彻底清净了那是不现实的。我见过不少团队在引入 Black 之后反而在“格式化之外”的问题上争论更多。原因是以前大家把讨论精力放在格式上格式上产生了大量噪音一旦噪音没了真正的设计问题浮出水面讨论门槛反而变高了。这是好事情但需要团队适应。比较实用的做法是在代码审查规范里写清楚格式问题交给工具判断人工 review 只关注逻辑、可读性和业务正确性。这样可以避免两种极端——要么什么都不看反正 Black 管了要么什么细节都要到合理程度就显然过头了。7.5 “格式化会弄坏代码”的顾虑怎么破部分开发者对“自动改写代码”有天生的不信任。我身边就有同事坚持认为 Black 是“破坏性工具”。事实是Black 的 AST 一致性校验基本杜绝了语义改变的可能但不代表完全没有风险。比如 Python 中非常罕见的情况下源码里依赖了行号、源代码文本的诡异怪癖例如依赖自身文件内容作为数据极端情况格式化就会影响行为。但这种代码本身就是反模式的它的正确性本来就脆弱。如果你遇到这种极端情况可以用# fmt: off/# fmt: on把那段代码包起来让 Black 跳过# fmt: off weird_code 1 \ 2 # 这里保持原样 # fmt: on对绝大多数项目来说这个功能一年都用不到几次但知道它的存在能显著减少大家对自动格式化的恐惧。7.6 PyCharm 里格式化“失效”的排查热搜词里有个“idea代码格式化失效”这类问题在 PyCharm 用户里很常见。我朋友遇到过的情况是插件装了保存时也确实有反应但代码格式没有被正确格式化。最后发现是 PyCharm 使用的 Black 指向了错误的解释器——项目里明明建了虚拟环境但 PyCharm 的 Black 路径配置还指到全局 Python导致它找不到 Black 包所以静默失败。处理方案很简单在 PyCharm 的 Settings → Tools → Black 里确认 Executable 路径指向当前项目的虚拟环境里的 black。然后用File Watchers或者Actions on Save触发。如果你用的是新版 PyCharm内置对 Black 的支持已经比较完善重点仍是解释器路径。8. 用好 Black 的几个核心认知8.1 Black 不是一个可选项而是一个“默认项”我现在的态度是任何新起的 Python 项目第一件事就是把 Black 配好。不是先写代码再考虑格式化而是项目初始化时就放进工具链。这和写文章之前先设定好字号、行距、页边距是一个道理——格式规范先行内容自然展开。如果你维护的是一个开源项目在 README 的贡献指南里写清“本仓库代码使用 Black 格式化”也是通用的做法。这能在早期过滤掉大量不必要的格式争论 PR。8.2 学会阅读 Black 生成的 diff使用 Black 初期你会看到大量格式化 diff如果不理解它为什么这么改很容易产生挫败感。我的经验是遇到看不懂的改动先不急着否定而是用black --diff生成差异结合代码上下文仔细观察通常都能理解它的换行策略。慢慢你会形成一种新的“代码格式感”之后写代码时会下意识按照 Black 的风格来写diff 就越来越少了。8.3 定期升级 Black但要有节奏Black 不是死水一潭它在持续演进规则也会微调。我的建议是日常使用不必追新每半年左右升级一次跟着 changelog 检查是否有影响项目的规则变化。升级之后全项目跑一遍格式化将变化集中提交别让版本的差异散落在各个分支里。8.4 最后分享一个配合策略没有单一工具是银弹Black 管格式、isort 管导入、Ruff 或 Flake8 管静态检查、mypy 管类型各司其职。使用 Black 一段时间后我最大的体会不是“代码变漂亮了”而是心理负担变小了。提交代码前不需要反复拷问自己“这是不是符合规范”“这个逗号有没有放对”工具已经把底线守住了我把思考留给真正重要的问题。如果你也想在自己的项目里引入 Black从今天就可以开始先 pip 安装在一个目录里跑一次black .看看 diff然后决定要不要保留。格式化工具这种东西用过一次真的回不去了。
返回列表