资讯详情

资讯详情

VSCode Python自动格式化配置:从Black到Ruff迁移与排查

我差不多每隔两个月就要被同事拉去看一次 VSCode 的格式化问题——现象千奇百怪但根因高度集中要么扩展装了三个互相打架要么settings.json和pyproject.toml里各写了一份行宽、值还不一样要么 CI 上跑的和本地跑的根本不是同一个工具版本。所以这篇就把VSCode Python 自动格式化配置从头捋一遍从Black怎么配、为什么这么配到迁到Ruff之后哪些行为会变、哪些坑会冒出来。不管你是刚开始建仓库、还没配过保存自动格式化还是手上有个跑了两年的项目想从 Black 换 Ruff 又怕把 diff 炸了下面这些内容都值得花二十分钟看完再动手。我尽量把每一步的为什么讲透因为格式化这东西抄配置十分钟搞懂机制能省你未来两年的返工。1. 先把这笔账算清楚为什么值得花半小时配好1.1 Black 解决的问题和它没解决的问题Black 当年能迅速统一社区审美靠的不是它格式化得最好看而是它把讨论成本降到了零。它几乎没有可调参数团队里没人能就括号该不该换行这一行该不该断吵起来——工具说了算。这个设计决策看起来霸道实际极其省事代码评审里再也不会出现纯风格的 diff 噪音新人提 PR 不用先背一本内部风格手册。但 Black 只管一件事把代码的空白和换行调整成统一形态。它不管 import 顺序不管你没有用到的变量不管你是不是把可变对象当默认参数传进去了。所以一个典型的 Python 项目在 Black 之外还得再挂 isort 排 import、挂 flake8 或 pylint 查问题、挂 pyupgrade 改老写法。四个工具、四份配置文件、四条 pre-commit 钩子每个都有自己的行宽参数和排除规则——黑名单写四遍改一次漏三处这事儿我干过不止一次。更麻烦的是速度。这几个工具虽然都能在一个大仓库里跑但逐个启动、逐个解析文件全量跑一遍几十秒起步。到了 CI 上为了省时间很多人干脆只在改动的文件上跑结果就是仓库里一半文件是格式化过的、一半没有diff 越来越脏。1.2 Ruff 把三件事压成一件Ruff 的定位很直白用一个工具同时干掉 lint、import 排序、格式化三件事底层用编译型语言实现跑起来比解释型的那几个快一个量级。我这边一个中等规模的仓库全量 Black isort flake8 跑完要十几秒换成ruff check --fix加ruff format之后加起来通常一两秒内结束冷启动也快得多。实际配置层面它的价值在于收敛一份pyproject.toml里[tool.ruff]、[tool.ruff.lint]、[tool.ruff.lint.isort]、[tool.ruff.format]四段把原来四份配置全收了。黑名单只写一遍行宽只写一遍# noqa和# fmt: off的语义也是同一套。团队交接的时候新人只看一个文件就知道代码规范是什么。代价当然也有得说清楚Ruff 的 lint 规则集虽然覆盖面很广但和 flake8 插件生态不是完全对齐的某些小众插件尤其是跟框架强绑定的那些它没有对应实现格式化部分对 Black 的兼容目标是绝大多数情况下输出一致不是 100%。剩下的那点差异通常集中在注释位置的取舍、长链式调用的括号布局、以及 docstring 内部的代码块处理上。这些差异对一个从零开始的项目毫无影响对一个有几千个文件的老仓库就意味着迁移那一下会产生一批非语义的 diff。1.3 选型对照什么仓库该用哪个我把常见的几种情况列一下你可以直接对号入座不用纠结。场景建议方案理由全新仓库从第一个 commit 开始Ruff 全套lint format没有历史包袱一份配置到底最省心老仓库已经全量 Black 过Ruff format先跑一次对比差异通常很小一次全量提交即可收敛团队严重依赖 flake8 特定插件Black 保留Ruff 只做 format别为了统一工具牺牲已有的检查能力仓库里有大量 Notebook两者都行但必须单独配 Notebook 作用域默认配置对.ipynb不生效多人协作、CI 严格任何方案都行关键是锁版本工具版本不一致才是真凶只想解决保存自动格式化Black 上手最快装一个扩展、写三行设置就完事最后一行值得单独说一句如果你的诉求只是保存的时候帮我把代码排整齐Black 依然是路径最短的选择一个扩展加三行配置就够了。Ruff 的收益要在你同时需要 lint 和 import 排序的时候才真正显现出来。2. VSCode 保存格式化的三层机制2.1 语言服务器、格式化扩展、保存钩子各管什么很多人配不明白是因为把三个独立的东西当成一个了。VSCode 里跟 Python 格式化相关的角色其实分三层。第一层是编辑器自身的保存钩子。editor.formatOnSave这个开关属于编辑器核心它不关心你用什么语言、什么工具只负责在文件保存的那一刻问一句当前这个文档有没有注册格式化提供者有的话叫它来干活。开关打开但没人应答文件就会原样保存什么也不会发生——这也是我明明开了 formatOnSave 却没用最常见的原因。第二层是格式化提供者也就是扩展。Black 的扩展、Ruff 的扩展装完之后会向编辑器注册自己声明我能格式化 Python 文档。它们的 ID 必须被editor.defaultFormatter精确指向写错一个字符都不行。同一语言可以同时有多个提供者注册编辑器默认会让你选选完记在设置里如果没选过、也没在设置里指定保存时就会弹一个选择框或者直接跳过。第三层是语言服务器比如 Pylance。它本身不负责格式化但它提供诊断和 code action包括整理 import这类操作。这一层和格式化是并行关系不是替代关系——你把 Pylance 关了格式化照样能跑但你会失去类型提示和一部分自动修复。搞清这三层之后排查思路就顺了保存不动先怀疑第一层开关和作用域弹框让你选工具说明第二层没指定默认能格式化但 import 没排说明第三层或对应的 code action 没开。2.2 保存那一刻的执行顺序这个顺序我建议你记牢因为它决定了很多为什么格式化完之后文件又变脏了的怪现象。保存被触发时编辑器先执行editor.codeActionsOnSave里登记的 code action再执行格式化。也就是说修复在前排版在后。这个顺序是有道理的lint 修复可能会删掉无用 import、改写语法结构、调整代码行数如果先格式化再修复修复产生的新代码就没被排过版文件保存下来还是脏的。反过来先修复再格式化最终落盘的一定是排好版的形态。理解了这一点很多配置就有了依据。Ruff 的source.fixAll.ruff和source.organizeImports.ruff应该放在codeActionsOnSave里而editor.defaultFormatter指向 Ruff两者配合才是一次保存完成全套。如果你把 import 排序交给格式化那一侧那是不对的——格式化器根本不改 import 顺序它只处理空白。还有个细节codeActionsOnSave的值在新版编辑器里推荐用explicit。它和老的true的区别在于explicit只在用户主动保存CtrlS时触发自动保存Auto Save不会去跑 code action。自动保存频繁触发全量 fixAll 会明显拖慢编辑手感尤其是文件大的时候。老配置里的true相当于always想省事也别用true直接用字符串字面量更稳。2.3 设置优先级User、Workspace、语言作用域VSCode 的设置是分层覆盖的从宽到窄依次是用户设置全局、工作区设置.vscode/settings.json、语言作用域设置[python]: {...}。窄的覆盖宽的语言作用域最优先。这个机制的好处是你可以在全局开editor.formatOnSave: true图省事然后在某个不想要格式化的仓库里用工作区设置把它关掉。坏处是排查的时候很容易看漏一层——你以为自己配了其实是被更窄的一层覆盖掉了而更窄的那层藏在某个你没注意的文件里。我的习惯是格式化相关的设置一律写在工作区也就是项目根目录的.vscode/settings.json跟着仓库走。理由很简单格式化是项目规范的一部分跟代码一起进版本库才合理换个机器、换个人拉下来行为一致。全局设置里我只留一两个纯个人偏好的东西绝不碰defaultFormatter。工作区设置还有个附带好处新人克隆仓库装上推荐扩展打开就能用不用再问你怎么配的。配套的extensions.json里写推荐扩展 ID编辑器会主动提示安装这一步能省掉大量沟通。3. 从 Black 起步一份能直接抄的完整配置3.1 装扩展的正确姿势Black 在扩展市场里有两种情况要分清老版本的 Python 扩展内部集成了 Black 作为可选项新版本已经把它拆成独立的格式化扩展。现在正确的做法是装那个名字里带 formatter 的独立扩展而不是去旧版 Python 扩展的设置里翻python.formatting.provider。如果你的settings.json里还留着python.formatting.provider: black这一行建议直接删掉。它是旧机制的遗留物新版编辑器根本不会读它留着只会让下一个人误以为已经配好了。这个坑我见过太多次有人照着三年前的教程配了一遍配置看起来满满当当实际一行都没生效。顺便提一下 Pylance它和 Black 不冲突可以同时装。但要注意 Pylance 自带一部分诊断如果后面你又开了 Ruff 的 lint同一个问题会在面板里出现两条看着很烦。解决方式在 7.3 里细说。3.2 settings.json 里该写哪几行下面这份是工作区里的.vscode/settings.json可以直接抄{ [python]: { editor.defaultFormatter: ms-python.black-formatter, editor.formatOnSave: true, editor.formatOnSaveMode: file }, black-formatter.importStrategy: fromEnvironment, black-formatter.args: [], files.eol: \n, files.insertFinalNewline: true, files.trimTrailingWhitespace: true }逐行解释一下为什么这么写。editor.defaultFormatter放在[python]作用域里而不是全局是为了不误伤 JSON、Markdown 这些别的语言。如果把它写在全局你编辑settings.json本身的时候也可能被拉去格式化虽然不会出错但没必要。editor.formatOnSaveMode显式写成file是个防御性动作。这个参数的可选值有file、modifications、modificationsIfAvailable后两个只格式化你改过的部分。有些团队为了减少 diff 噪音会设成modifications代价是老文件里的历史代码永远不会被整理慢慢就形成新旧两套风格混在一个文件里的奇观。新仓库没理由用这个模式显式声明一下省得被别处的配置带偏。importStrategy设成fromEnvironment的意思是用当前选中的 Python 解释器环境里安装的那个 Black而不是扩展自带的版本。这一条极其重要。扩展自带的版本是扩展更新时锁定的跟你requirements-dev.txt里写的版本大概率对不上本地跑出来的结果和 CI 跑出来的结果就会有细微差异——最典型的是某个版本改了 magic trailing comma 的处理方式你的本地 diff 和 CI 报错永远对不上号。black-formatter.args留空数组是有意的。原因在 4.5 节展开这里的参数会以命令行形式传给 Black优先级高于pyproject.toml两边都写就会出现改了 pyproject 没用的情况。后面三个files.*是配套项。files.eol统一成 LFinsertFinalNewline保证文件末尾有换行trimTrailingWhitespace清掉行尾空格。这三条严格说不属于格式化工具管辖但它们处理的是同一类问题混在一起不做的话格式化器每次保存都在跟它们打架。3.3 pyproject.toml 的 Black 段落配置文件里这一段的写法[tool.black] line-length 100 target-version [py311] include \.pyi?$ extend-exclude /( \.git | \.venv | \.mypy_cache | build | dist )/ line-length这里我写 100。Black 默认是 88这个数字在 2018 年定下来的时候是基于能在小屏幕上并排看两个文件的考量。现在显示器普遍宽了很多团队会调到 100 或 120。选哪个都行关键是全仓库统一并且和 lint 的行宽告警阈值对齐。target-version建议显式写上别让 Black 去猜。它会影响一部分语法的处理方式比如某些新语法在旧版本目标下会被加上不同的括号。这个值应该跟你pyproject.toml里requires-python或者 CI 的最低测试版本保持一致。extend-exclude用了正则的多行写法把虚拟环境目录、构建产物都排除掉。这里有个经验不要用--force-exclude之外的任何方式依赖编辑器层面的排除。我用过.vscode/settings.json里的files.exclude来藏虚拟环境结果那只影响资源管理器显示Black 该扫还是扫一个大仓库里扫进.venv就是灾难性的慢。真正的排除要写在工具自己的配置里。3.4 怎么验证它真的生效了配完不要直接开工花两分钟验证一下能省掉后面一堆怀疑人生。第一步随便打开一个.py文件故意把缩进弄乱、加几个多余空格保存。看文件有没有被自动整理。没有反应的话右下角状态栏能看到当前选中的 Python 解释器点进去确认一下选的是项目虚拟环境不是系统 Python。第二步打开命令面板执行格式化文档看有没有弹出选择格式化器的提示。如果弹了说明defaultFormatter没被识别八成是扩展 ID 写错或者扩展没装。第三步也是最能说明问题的一步随便写一行超长的代码长度在 88 到 100 之间保存。如果它被断行了说明生效的是 88默认值或者扩展自带配置如果没断说明你配的 100 生效了。这一招能立刻分辨出配置到底有没有被读到比看设置界面靠谱得多。第四步验证版本对齐。在终端里跑一下环境内的 Black看版本号再对比 CI 配置文件里锁的版本。两个数不一致就先统一别急着往下走。4. 迁移到 Ruff把行为差异压到最小4.1 迁移前先做行为对照直接切换是最容易出事的方式。稳妥的做法是先做一次只读对照在干净的 worktree 里用当前锁定版本的 Black 跑一次全量格式化提交然后换 Ruff format 再跑一次git diff --stat看看有多少文件会被改动、改动量多大。我的经验值是这样绝大多数仓库的差异文件占比在个位数百分比改动内容集中在注释对齐、长表达式换行位置、以及空行数量。如果 diff 规模远超这个量级先别急着迁检查一下是不是line-length两边配得不一样——这是最常见的假差异来源。对照做完心里就有数了。接下来分两步走先让 Ruff 只接管 lint 和 import 排序格式化还留给 Black跑一两周稳定再让 Ruff format 接管格式化。分步走的好处是出问题时能立刻定位是哪一环引入的。4.2 扩展与 settings.json 配置装 Ruff 的官方扩展。市场里同名的不少认准扩展详情页里那个 IDsettings.json里写错一个字母就完全没反应而且不一定报错。{ [python]: { editor.defaultFormatter: charliermarsh.ruff, editor.formatOnSave: true, editor.formatOnSaveMode: file, editor.codeActionsOnSave: { source.fixAll.ruff: explicit, source.organizeImports.ruff: explicit } }, ruff.importStrategy: fromEnvironment, files.eol: \n, files.insertFinalNewline: true, files.trimTrailingWhitespace: true }和 Black 那份的差别只有三处但每一处都有讲究。codeActionsOnSave里用了带后缀的 action kindsource.fixAll.ruff而不是笼统的source.fixAll。带后缀的写法把执行权精确限定给 Ruff避免其他扩展也注册了同名 action 时排队执行、互相覆盖。多语言仓库里这一点尤其重要全局的source.fixAll可能同时触发好几种语言的修复器。两个 action 都设成explicit理由前面说过自动保存不跑修复。如果你的工作流依赖频繁自动保存这个设置能明显降低输入延迟。organizeImports和fixAll同时开会不会重复干活实际不会。Ruff 内部有去重import 排序的结果和 fixAll 里的 I 规则输出一致。但如果你只想要 import 排序、不想要其他自动修复那就只留source.organizeImports.ruff这一条把fixAll去掉——刚接手老仓库的时候我经常这么干避免一次保存改出几百行 diff 把人吓到。4.3 pyproject.tomllint 与 format 两套段落[tool.ruff] line-length 100 target-version py311 extend-exclude [.venv, build, dist] [tool.ruff.lint] select [E, F, I, UP, B, SIM] ignore [E501] [tool.ruff.lint.isort] known-first-party [myapp] force-sort-within-sections true [tool.ruff.format] quote-style double indent-style space line-ending lf docstring-code-format true这里有两个版本相关的坑必须先说。第一[tool.ruff.lint]这个嵌套结构是较新版本才有的。早期版本里select、ignore是直接写在[tool.ruff]顶层的。如果你装了老版本的 Ruff把配置写成新结构它会报未知字段或者干脆忽略反过来新版 Ruff 读老结构会给出弃用警告。判断方法很简单先跑一次ruff check .看输出里的版本号再对照文档确认结构。第二ruff format这个子命令不是一开始就有的。早期版本只有 lint。如果你在终端里敲ruff format .提示命令不存在那不是配置问题是版本太老升级就行。这个坑很隐蔽因为扩展装上之后defaultFormatter指向它不会报错只是保存时静默什么都不做。配置内容逐条说。select里我列了六组E是 pycodestyle 的错误类F是 pyflakes 那套未使用变量、未定义名称的检查I是 isort 规则UP是 pyupgrade 那类老写法升级B是常见陷阱检测可变默认参数、循环里用闭包之类SIM是简化建议。这六组覆盖了绝大多数日常问题误报率也低。再往上加ANN类型标注、Ddocstring、S安全这些就属于团队偏好了新项目建议从这六组起步跑顺了再逐个加。ignore [E501]是必须的。E501 是行太长告警但格式化器已经把代码行拆到line-length以内了唯一还能超长的是长字符串和长注释——这两样格式化器管不了也不会去动。留着 E501 只会让你的 CI 天天红而且红得没有意义。要么忽略它要么把line-length调到一个连注释都能装下的值。我选忽略。known-first-party要填你自己的顶层包名这样 import 分组才能正确分出标准库 / 第三方 / 本项目三块。不填的话你自己的包会被当成第三方跟 requests 排在一起看着就难受。quote-style double是 Black 的行为保持默认就行。如果老仓库里全是单引号、想保持现状改成preserve它不会去统一引号风格——这个值对应 Black 的skip-string-normalization。line-ending lf在 Windows 上要小心。如果仓库里已有文件的换行是 CRLF这个设置会让它们全部被重写产生一个巨大的 diff。配这个之前先确认.gitattributes里的处理策略具体在 6.3 说。docstring-code-format true会把 docstring 里用或三个反引号包起来的代码示例也格式化一遍。这个功能 Black 没有属于 Ruff 的加分项用了一段时间之后文档里的示例代码再也不会跟实际代码风格脱节。但它也可能改到你没预期的地方如果 docstring 里的示例是故意保留某种排版的用# fmt: off或者干脆关掉这个开关。4.4 import 排序从 isort 迁到 I 规则isort 的配置文件通常是setup.cfg或.isort.cfg里面known_first_party、force_sort_within_sections、multi_line_output这些参数在 Ruff 里绝大部分都有对应isort 参数Ruff 对应说明known_first_party[tool.ruff.lint.isort] known-first-party值从字符串改为数组force_sort_within_sectionsforce-sort-within-sections同名布尔值force_alphabetical_sort_within_sectionsforce-sort-within-sections语义接近不完全等价multi_line_output无需配置Ruff 用自己的固定布局include_trailing_comma无需配置由格式化器决定line_length[tool.ruff] line-length全局统一multi_line_output和include_trailing_comma没有对应项因为它们决定了括起来的 import 该怎么换行、要不要尾逗号——这属于格式化范畴交给ruff format统一处理。迁移时这两个参数直接删掉别试图保留保留不了。迁移的具体操作先把select里加上I跑一次ruff check --select I --fix .看 diff。这个 diff 会包含所有 import 顺序的调整通常不大但会涉及很多文件。确认没问题后提交再把 isort 从 pre-commit 和 CI 里摘掉两套工具同时管 import 是纯粹的混乱来源。有个细节如果项目里用了from __future__ import annotationsisort 的某些配置会把它单独拎出来放最前面。Ruff 的 I 规则默认就处理这个不需要额外配置。4.5 双份配置谁覆盖谁line-length 的坑这一节值得单独拎出来因为它让无数人对着配置文件怀疑人生。VSCode 扩展的配置项ruff.lineLength、black-formatter.args里的--line-length在真正执行时是以命令行参数的形式传给工具的。而命令行参数的优先级高于配置文件。所以只要你两边都写了结果一定是settings.json赢pyproject.toml里改什么都没用。我见过最典型的翻车现场有人在pyproject.toml里把行宽从 88 调到 100本地保存还是按 88 断行以为工具坏了折腾半天。实际上他扩展设置里早就写死了一个--line-length 88。我的建议是只留一处行宽、目标版本这类跟代码本身相关的参数全部写在pyproject.toml里settings.json里一个字都不写。这样本地通过扩展读到的是项目配置CI 上直接读同一个文件天然一致不存在本地过了 CI 挂了的经典问题。那什么时候才需要在settings.json里写只有一种情况项目根目录不是 Python 项目的根。比如你打开的是一个 monorepo 的顶层目录pyproject.toml在services/api/下面扩展可能找不到它。这时候可以用ruff.configuration之类的设置显式指向配置文件路径而不是去覆盖具体参数。指向配置文件永远比复制粘贴参数可靠。5. 本地和 CI 说同一句话5.1 pre-commit 钩子的顺序编排本地靠编辑器保证提交前代码已经格式化但这个保证不可靠——有人用别的编辑器有人手动git commit -a跳过保存钩子有人在命令行里直接 merge 进来一批没格式化的代码。所以还需要一道提交时的关卡。pre-commit 的配置大致长这样repos: - repo: https://github.com/组织/ruff-pre-commit rev: v0.x.y hooks: - id: ruff args: [--fix] - id: ruff-formatrev那一行填你锁定的版本它必须和本地开发环境里装的 Ruff 版本一致。不一致就是灾难CI 用新版、本地用旧版格式化结果有差异代码在本地永远看起来是好的一到 CI 就报需要重新格式化。我给团队定的规矩是版本号在pyproject.toml的依赖里锁一次、在 pre-commit 配置里锁一次两处改必须同时改评审的时候专门看这一条。钩子的顺序有讲究ruff带--fix放在ruff-format前面。理由是 lint 修复可能会删代码、改结构改完之后需要重新排版反过来做的话排版完之后 lint 又改了东西最终落盘的代码不是排过版的。这和编辑器里修复在前格式化在后是同一个逻辑只是把编辑器的顺序在提交阶段重演一遍。一个实用的细节pre-commit 第一次跑会卡很久因为它要把钩子的运行环境装一遍。别以为是自己仓库有问题等它跑完第二次就快了。另外它对每个文件单独调用一次工具文件多的时候明显比直接跑ruff format .慢这是架构决定的能忍就忍忍不了就在本地习惯性先手动跑一遍全量。5.2 CI 里跑 check 而不是 formatCI 上的命令必须是只检查、不修改ruff check . ruff format --check .--check的语义是如果有文件需要修改退出码非零它不会动文件。CI 环境里跑格式化并提交是绝对错误的做法——你以为在帮忙实际制造了一堆没人 review 过的代码变更还把构建产物的来源搞得不清楚。Black 那边对应的是black --check --diff .。--diff会把差异直接打在日志里比只报一句失败有用得多新人看一眼就知道该改哪。CI 的报错信息有一个大坑ruff format --check报would reformat 3 files但不告诉你是哪三个文件——不是它不想说而是默认输出级别不够。加--diff或者调高 verbosity 才能看到具体文件。团队里新人第一次遇到这个几乎都会懵所以在CONTRIBUTING里写清楚本地先跑一遍ruff format .再推能省掉大量来回。还有一个容易被忽略的点CI 必须用和本地一样的 Python 版本和一样的工具版本。用pip install ruff而不锁版本今天装的 0.x 和明天装的可能行为就变了。锁定版本这件事说多少遍都不算多。5.3 一次性的全量格式化提交怎么不毁 blame从 Black 迁到 Ruff format或者第一次给一个跑了几年的老仓库引入格式化必然会有一个巨大的全量提交。这个提交之后git blame会显示每一行都是你改的同事查历史的时候会想骂人。解决办法是.git-blame-ignore-revs。把那次全量格式化的 commit hash 写进这个文件提交到仓库根目录然后让 git 忽略它git config blame.ignoreRevsFile .git-blame-ignore-revs这个配置可以写进仓库的文档让人手动执行也可以在新版 git 里通过仓库级别配置自动生效。效果是git blame会跳过那些纯粹是格式化的提交继续往上追溯到真正改逻辑的那一次。配套要做的是那个全量格式化提交消息里一定要写清楚仅格式化无逻辑变更并且单独一个提交不要和任何功能改动混在一起。混在一起的提交既没法 review也没法安全回滚。我一般会在提交前先git diff --stat看一眼文件数量再随机抽查几个文件确认 diff 里只有空白变化用git diff -w看一遍空输出就能确认。6. 那些看起来无关紧要却天天咬人的参数6.1 行宽口径统一行宽看起来是个小问题实际是格式化和 lint 冲突的头号来源。代码里涉及行宽的地方有四处格式化器的line-length、lint 的行长告警阈值、编辑器的那条竖线editor.rulers、以及你脑子里多少算太长的直觉。这四处不一致就会形成一种很别扭的状态编辑器里看起来没超线保存一格式化断了行lint 又没报错但你写的时候完全不知道边界在哪。我的做法是把前三处对齐成同一个数字line-length 100、关掉 E501因为注释和长字符串注定超、editor.rulers: [100]。这样写代码的时候有一个明确的视觉边界越线了就知道该拆。至于这个数字选多少我的经验是纯后端服务代码 100 比较舒服因为函数调用链和类型标注比较长数据处理类的代码 88 也行因为表达式本身不长写前端或者要用到大量链式调用的场景有人用 120。真正重要的不是数字本身而是全仓库只有一个数。6.2 引号、尾逗号与 magic trailing comma引号统一这件事Black 和 Ruff 的默认行为都是把单引号改成双引号除非字符串里本身包含双引号。这个默认值当年争议很大现在基本接受了。老仓库如果想保持原样Ruff 里配quote-style preserveBlack 里配skip-string-normalization true。更值得说的是 magic trailing comma。规则是如果你在函数调用或集合字面量的最后一个元素后面留了逗号格式化器就会把它拆成每行一个如果没有逗号只要能塞进一行就塞进一行。这个行为的实际影响很大。举个最常见的场景result call_function(argument_one, argument_two, argument_three,)末尾那个逗号会让它变成result call_function( argument_one, argument_two, argument_three, )反过来你把逗号删掉它又会压回一行。所以这段代码会不会被拆行的控制权其实在你手里不在格式化器手里。很多人抱怨格式化器总是把代码拆得太散其实是因为编辑器在他们敲参数时自动补了尾逗号。这个特性在两边的实现细节上有过细微差异某些版本对参数里带注释时是否还拆的处理不完全一样。跨版本迁移时如果发现这一类 diff优先怀疑版本差异而不是配置写错了。6.3 换行符与文件末尾换行符这事在跨平台团队里是固定节目。Windows 上默认 CRLFLinux 和容器里是 LF。格式化器配了line-ending lf就会强制转成 LF如果 Git 那边又开了自动转换就会形成保存时转 LF、检出时转 CRLF、每次保存都算一次修改的死循环git 状态永远是脏的。根治的办法是在仓库根目录放一个.gitattributes* textauto eollf *.bat text eolcrlf *.ps1 text eolcrlf把所有文本文件的检出和提交都统一成 LF只对确实需要 CRLF 的脚本例外。配上git config core.autocrlf falseLinux/macOS 默认就是 falseWindows 上要手动设这个循环就断掉了。文件末尾的换行是另一件小事。POSIX 标准里文本文件应该以换行结尾但很多编辑器不强制。格式化器通常会补上但如果你同时开了files.insertFinalNewline: true和格式化两边都会加不会冲突只是会让人搞不清是谁加的。行尾多余空格同理files.trimTrailingWhitespace: true会处理掉格式化器也会处理掉两个一起开着没坏处。6.4 Notebook 与一次性脚本Jupyter Notebook 的格式化走的是另一套开关。editor.formatOnSave对.ipynb不生效得用 notebook 专属的配置{ notebook.formatOnSave.enabled: true, notebook.codeActionsOnSave: { notebook.source.fixAll.ruff: explicit, notebook.source.organizeImports.ruff: explicit } }注意这两个键名前面有notebook.前缀写在[python]作用域里是没用的。这一条我踩过当时纳闷了很久为什么 py 文件格式化正常、ipynb 死活不动。Notebook 的格式化还有个现实问题它会把每个 cell 当成独立的代码块处理跨 cell 的 import 排序结果可能和你预期不一样。做数据分析的时候我一般只开格式化、关掉 fixAll避免一个 cell 里的临时变量被判定为未使用然后被删掉——虽然后者通常是 lint 规则而不是格式化器干的但在 notebook 环境里两者的边界确实容易混。至于那种随手写的临时脚本test.py、debug.py我建议单独用files.exclude或者.gitignore把 scratch 目录排除掉别让它们进格式化范围。这些文件的价值在于快速验证一个想法加了格式化反而拖慢节奏。7. 现场排查保存没反应与结果反复横跳7.1 保存完全没反应这是最高频的问题排查路径我按命中率排了序检查项具体做法命中率扩展没装或没启用扩展面板搜一下看是否已安装、是否在工作区被禁用高defaultFormatterID 写错对照扩展详情页的完整 ID逐字符核对高设置被更窄作用域覆盖打开设置界面搜这个键看是否显示已在工作区修改中文件语言模式不对右下角看是不是 Python新文件未保存时可能是 Plain Text中新版旧版配置混用搜python.formatting.provider之类废弃项并删除中格式化超时看输出面板里的日志大文件可能超过默认超时低文件只读或超大检查文件大小编辑器对超大文件有格式化限制低超时这一条值得单独讲。编辑器有个默认的格式化超时大约 750 毫秒超过就取消并提示。几百行的小文件不会触发但上千行的老代码用 Black 格式化确实可能超。表现是偶尔能动、偶尔不动很迷惑。解决办法要么换成更快的工具要么把超时值调大要么接受这个现实、用终端手动跑。7.2 格式化跑两次或结果回退结果反复横跳的症状是保存一次代码被格式化了再保存一次又变回去了。原因通常是两个格式化器同时生效。最常见的情形是装了 Black 扩展又装了 Ruff 扩展两边都在formatOnSave的时候抢着干活。谁最后执行取决于注册顺序所以表现会很随机。解法是只留一个defaultFormatter另一个扩展可以留着但不设成默认或者干脆禁用。第二种情形是codeActionsOnSave里的 fixAll 和格式化器互相打架。比如某个 lint 规则建议把a b合并成一个字符串格式化器又觉得该拆行两个轮流改永远收敛不了。这种时候要么把那个规则关掉要么把 fixAll 从保存钩子里摘出来改成手动触发。第三种情形比较少人知道formatOnSave和formatOnType同时开。Python 的格式化器基本不支持formatOnType那是给格式化型编辑器用的但如果哪个配置模板里给你开了可能会出现敲一个字符整段重排的诡异体验。检查一下editor.formatOnType是不是 true改回 false。7.3 多语言混排仓库的冲突仓库里同时有 Python、TypeScript、Markdown 的时候全局的source.fixAll会触发所有注册了该 action 的扩展。表现是保存一个.py文件结果 Markdown 的格式化器也跑了或者保存.ts文件时 Ruff 莫名其妙地试图干活。解法有两个层次。第一层把 action kind 带上后缀source.fixAll.ruff只触发 Ruff不触发别人。第二层语言作用域分清楚[python]里只放 Python 相关设置[typescript]、[markdown]各管各的。还有个 Pylance 和 Ruff 诊断重复的问题。两个都开的时候未定义变量这类问题会在问题面板里出现两条一模一样的。如果觉得吵可以在设置里降低 Pylance 的诊断级别{ python.analysis.diagnosticSeverityOverrides: { reportUndefinedVariable: none, reportUnusedImport: none } }但我不建议一上来就关。Pylance 的类型检查是另一个维度的能力Ruff 补不了。更合理的做法是让两边各管各的Pylance 管类型相关的告警Ruff 管风格和常见错误重叠的那几个再单独关掉。7.4 容器与远程环境下的路径问题用开发容器或者远程开发的时候fromEnvironment这个策略会遇到一个现实问题编辑器跑在宿主机上工具装在容器里两边路径不通。表现是扩展找不到 Ruff 或者 Black然后悄悄回退到自带版本。判断方法是看扩展的输出日志里面会打印它实际调用的是哪个可执行文件、什么版本。版本号和你环境里pip show出来的不一致就是回退了。解决路径有几条一是在远程场景下把importStrategy明确设成useBundled接受用扩展自带版本但必须在 CI 里用同一个大版本号二是确保编辑器选中的解释器就是容器里的那个虚拟环境扩展会顺着解释器路径去找工具三是干脆放弃扩展的自动查找用设置显式指向可执行文件的绝对路径——但这会让配置不可移植只在个人环境里用用还行别提交到仓库。我一般选第二条因为它对团队最友好所有人都在容器里开发解释器路径固定扩展自动找得到版本靠requirements-dev.txt锁死本地和 CI 天然一致。8. 团队落地时我踩过的几个坑最后说几个不成体系但真实存在的经验。别一次性给团队推全套规则。Ruff 的规则集可以开得很凶但你要考虑到同事手上还压着别的活。我的做法是先只开E、F、I三组跑两周让大家习惯保存时会改代码这件事再逐步加UP、B。每次加规则的那个 PR 单独提diff 里的改动解释清楚别混在功能提交里。给格式化和改逻辑设一条硬边界。任何提交里如果既有格式化改动又有逻辑改动评审成本会翻倍。pre-commit 能在提交阶段拦住大部分情况但拦不住 merge 进来的分支。我的经验是每周固定跑一次全量ruff format .如果有 diff 就单独提交别攒着。新人的第一个 PR 大概率会因为格式挂 CI。这不怪他们是流程没铺好。仓库根目录的.vscode/settings.json加上extensions.json的推荐扩展列表能让新人克隆下来就是配好的状态。这一步的投入产出比高得离谱几乎是配一次管一年。别把# fmt: off当万能药。有些人遇到格式化器把表格类数据排乱了就在整个文件头部加# fmt: off然后这个文件就永久脱离格式化范围了。正确的粒度是包住那一段代码# fmt: off和# fmt: on之间只放确实需要保留排版的部分比如对齐的常量表、手写的矩阵。最后一个技巧如果你想快速确认某段代码到底是被哪个规则或格式化行为改的在终端跑ruff format --diff 文件名和ruff check --diff 文件名两条命令的输出能直接告诉你改动来源。比在编辑器里反复撤销重做强太多尤其在排查到底是 lint 还是 format 动的这种问题时一眼就能分清。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →