ARTICLE DETAIL

资讯详情

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

Pylint与Flake8如何分工搭配?Python代码检查实战指南

Pylint与Flake8如何分工搭配?Python代码检查实战指南 接手过老项目的人都懂代码本身能跑、测试能过但一改起来就像拆雷。命名乱成一锅粥、一个函数里塞十几个参数、无用的 import 堆了满满一屏想重构又怕碰坏哪个隐藏逻辑。这种时候静态检查工具就是最后一道心理防线。Python 生态里最常被拉出来站岗的就是 Pylint 和 Flake8 这两个名字。一个是出了名的苛刻一个是出了名的轻快但很多新手把两者当成“二选一”的单选题这其实是个很大的误解。这篇文章我会实打实地聊聊这两个工具到底在查什么、怎么搭配用、配置文件怎么写以及一个真实项目从几百条告警清理到接近零的全过程顺便把那些网上不会写明白的坑也交代清楚。适合正在给项目引入代码规范、被 lint 告警逼到怀疑人生、或者刚接手历史包袱比较重的老代码库的同学。1. 为什么需要两道检查Pylint 和 Flake8 的分工逻辑1.1 从一次“看起来没问题”的提交说起先讲一个我真实遇到过的场景。有次同事提交了一个订单导出功能本地测试、联调、上线全都没问题。三天后另一个同事要在这个模块里加一个字段翻代码时发现finally块里藏了一个return外层函数的异常处理等于白写再往下看有一个 import 的包从头到尾没用上还有一个变量名写成了l——没错就是小写的字母 L在部分字体下和数字 1 几乎一模一样。这些问题是测试抓不到的。单测只关心“输入对不对、输出对不对”不关心代码写得丑不丑、有没有藏雷。而人眼 review 又太依赖个人状态和经验今天心情好看得细明天赶进度可能就一眼扫过。Pylint 和 Flake8 这类工具的价值就在于它们不睡觉、不烦躁、不看人脸色每次提交都严格执行同一套标准。用我自己的话说Flake8是守门员负责拦明显犯规的动作Pylint是审计师负责揪出那些“不算犯规但会埋雷”的习惯。很多团队纠结“到底用哪个”其实是个伪命题。它们压根不是同一层的东西。1.2 两个工具做的是不同维度的事先看一张对比表后面再展开讲。对比维度PylintFlake8底层构成独立开发的静态分析器pycodestyle pyflakes mccabe 三合一检查重点逻辑错误、代码异味、重构建议、命名规范PEP8 风格、语法级错误、未使用变量、圈复杂度输出信息带消息 ID、符号名和建议代码还能玩游戏式打分相对轻量一行一条路径:行号:列号 代码运行速度较慢全量跑大项目可能需要几十秒甚至更久很快秒级完成误报倾向偏高常有“教做人”式的抱怨偏低规则偏向客观事实配置复杂度配置项非常多默认规则偏严配置简单几条参数就能开工与格式化工具兼容性兼容但需要较多配置行宽、缩进等规则容易让开发者想关掉它Flake8的定位是快、准、客观。它检查的内容基本属于“硬伤”代码风格不符合 PEP8行太长、缺空行、缩进不对、模块导入了没用、局部变量赋值了没读、函数圈复杂度超过阈值。这些规则几乎不存在争议——缩进错了就是错了变量没用就是没用。跑一遍只要一两秒放进 pre-commit 钩子里几乎感觉不到存在。Pylint的定位是深、全、严格。它基于 Python 的 AST抽象语法树做更细腻的分析会检查到很多Flake8看不见的东西函数参数过多、分支复杂度太高、重复代码、except裸捕获、返回语句不一致、动态属性访问不了甚至还会检查模块和函数的 docstring 写没写。它最“招恨”的 10 分制评分功能本质上是在给代码的“健康程度”打一个直观的量化分。所以我的建议从来都是两个都装Flake8 把关“能不能提交”Pylint 把关“要不要重构”。单独用任意一个都会漏掉另一半问题。Flake8 不会告诉你这个函数逻辑太复杂了Pylint 不会在你上线前帮你快速拦住那些满含空格的换行。2. 从零到一安装、配置与第一轮扫描2.1 五分钟跑起第一轮扫描安装没有什么悬念直接 pip 装就行。建议装在当前项目的虚拟环境里不要全局装否则换项目以后配置会互相干扰。pip install pylint flake8装完后先找一个小项目试一下。我拿一个简单的demo.py举例里面故意放几个典型问题import os import sys def process_data(data): unused_var 42 if data: print(processing) return True else: return False result process_data(hello)跑一下 Flake8flake8 demo.py输出大概是这样的demo.py:1:1: F401 os imported but unused demo.py:2:1: F401 sys imported but unused demo.py:4:5: E303 too many blank lines (3) demo.py:4:5: C901 process_data is too complex (3)每一条都包含文件路径:行号:列号: 规则代码 具体说明。你根本不用查文档光看说明就知道该怎么改。再跑一下 Pylintpylint demo.py输出就会华丽很多除了具体的告警列表结尾还会给一个 10 分制的评分。第一次跑的时候大多数人都会被那一整屏的C、W、R开头的消息吓到——别慌这就是它的工作方式后面会说怎么跟它“讨价还价”。2.2 把配置文件固定进仓库跑通一次之后第一件事不是写业务代码而是把两个工具的配置固化到仓库里。没有配置文件的 lint 工具就像一个没有规矩的保安今天按这个标准拦明天按那个标准拦队友之间必然吵架。Pylint 的配置生成很简单pylint --generate-rcfile .pylintrc这个.pylintrc文件会把所有默认配置项都列出来文件很长你不需要也没必要全看完。真正需要改的是下面这几个点。以我常用的配置为例[MASTER] ignore migrations,venv,.git jobs 4 persistent no [MESSAGES CONTROL] disable C0103, C0114, C0116, R0903 [DESIGN] max-args 8 max-locals 18 max-returns 8 [FORMAT] max-line-length 100逐条说下意图。ignore不用多说跳过虚拟环境和迁移脚本jobs 4是并行解析跑大项目能快不少persistent no是不写缓存文件CI 环境下更干净。disable里我默认关掉了几个噪音大户C0103命名风格、C0114缺模块 docstring、C0116缺函数 docstring、R0903类里只有少部分公共方法。不是这些规则不应该存在而是新人刚引入的时候这些规则会产生大量和历史包袱无关的告警挫败感极强。等团队适应了可以再把 docstring 类规则加回来。Flake8 没有自动生成配置的命令但你不需要它。直接在项目根目录建一个setup.cfg或者建一个.flake8文件内容如下[flake8] max-line-length 100 max-complexity 12 exclude .git,__pycache__,migrations,venv extend-ignore E203,W503我把行宽和 Pylint 保持了一致都放宽到 100 而不是 PEP8 默认的 79。这里多说一句79 字符是早期终端宽度的历史遗留现在的团队普遍用 88Black 默认或者 100只要全项目统一比纠结“79 才是正统”有意义得多。extend-ignore里的E203和W503是为了兼容 Black 格式化Black 的冒号前不留空格规则和 E203 冲突W503和W504则是一对历史上反复横跳的换行符规则二选一我选了W503关掉。配置完记得提交到版本库。这一条我会在所有地方反复强调配置文件不进仓库等于没配。3. 看懂输出学会和规则讨价还价3.1 错误代码的暗号五秒钟快速定位问题两个工具输出里的那些字母加数字的代码就是它们的“暗号”。看懂这些比挨个查文档效率高得多。Pylint 的消息 ID 用 4 位数字开头字母对应消息类型开头字母含义常见例子Cconvention 约定C0103 命名不规范、C0116 缺函数 docstringRrefactor 重构R0913 参数过多、R0914 局部变量过多、R0911 返回语句过多Wwarning 警告W0611 未使用的导入、W0612 未使用的变量、W0702 裸 exceptEerror 错误E0602 使用未定义的变量、E0401 无法导入模块Ffatal 致命通常是分析器本身无法处理某些语法结构我的经验是E 和 F 类是无条件要修的这是真 bugW 类 90% 要修属于“以后必炸”的雷R 类值得开会讨论属于“确实该重构但排期另说”C 类看团队约定命名风格这种东西统一规则比绝对正确更重要。Flake8 的代码前缀则更简单直接开头字母所属检查器常见例子Epycodestyle 风格错误E501 行太长、E303 空行过多、E128 延续行缩进错误Wpycodestyle 风格警告W503 运算符前换行、W504 运算符后换行Fpyflakes 逻辑检查F401 导入未使用、F841 局部变量未使用、F821 未定义的名字C901mccabe 复杂度函数圈复杂度超标有一个很关键的提示Flake8 的F开头规则和 Pylint 的W0611、W0612有大量重叠但它们查重的方式不同。我处理这类告警时有一个原则Flake8 报的 F 类一定马上改因为它的分析器非常保守报出来就是板上钉钉的事实Pylint 报出来的 W 类先看一眼偶尔会有误判。3.2 处理误报的正确姿势说实话lint 工具用久了你会遇到一些“理论上该报、实际上我不需要改”的情况。最常见的误报来源是动态属性。比如用 Django 的时候request.user、Model.objects这类由框架自动生成的属性Pylint 是不认识的会给你报E1101: Instance of WSGIRequest has no user member。你在.pylintrc的MASTER段里加上generated-members objects,user,request.GET,request.POST把框架动态挂上去的属性和方法加到这个列表里误报就消停了。还有一类误报是业务场景决定的。比如一个接口函数定义了 9 个参数R0913参数过多肯定会响。但如果你需要兼容旧客户端调用不能随便改签名这时候用局部豁免def update_user(user_id, name, email, phone, address, avatar, tags, source, remark): # pylint: disabletoo-many-arguments ...Pylint 支持在代码行尾加# pylint: disable规则名按行精准豁免。Flake8 对应的语法是行尾加# noqa也可以更精确地写# noqa: F401只豁免某一条规则。这里非常重要的一点是豁免必须精准到规则、到行不允许一个 disable 放倒整片。我见过有人图省事在配置里加了disable all结果工具变成了一个纯摆设所有问题都从防线下面溜过去。这种“仪式性 lint”比不装工具危害更大——它给了团队一种虚假的安全感。另外和 Black 格式化工具一起用的时候会有几个固定冲突场景。Black 默认行宽 88Pylint 默认 79Flake8 默认 79三个标准如果不统一Black 刚格式化完Flake8 就开始报 E501。解决办法就是前面说的在 Pylint 和 Flake8 里都把max-line-length改成 88 或者 100和 Black 保持一致。还有 Black 会把集合里的元素写成一个一个独立行这种格式在某些 Flake8 版本里会触发 E203所以我在配置里把E203加入extend-ignore。4. 实战一个老项目从 400 条告警到零的完整流程4.1 先跑一遍全量扫描把告警分类归档理论讲完上真实案例。之前我接手过一个基于 Flask 的管理后台大概 60 个模块、2 万行 Python 代码。历史原因这项目基本没跑过任何静态检查。第一次全量扫描的时候Flake8 给出了 380 多条告警Pylint 打了一个让我印象深刻的 3.9 分。当时的处理方式是把告警分成四类按优先级推进。优先级告警类型大约数量处理方式P0Flake8 F 类逻辑问题30 条左右全部当场修有 10 多条确实是潜在 bugP1Flake8 E/W 类风格问题230 条左右花一到两天用脚本批量修部分手动P2Pylint E 类20 条左右全部修其中几条和 F 类重叠P3Pylint W/R/C 类剩下所有分阶段修允许先加豁免再慢慢消化P0 里最典型的几个F821 undefined name有两处代码里引用了一个根本没定义的变量因为被try/except包着运行时机缘巧合没走到那条分支属于运气好没炸的雷F841 local variable is assigned to but never used有十几处大多数是调试时留下的中间变量直接删还有一个F811 redefinition of unused同一个函数在文件里被定义了两次后一个覆盖前一个如果哪天有人想调用旧版本会发现怎么行为不对。这个阶段我强烈建议用脚本批量统计告警分布而不是手动翻输出。跑一遍flake8 --statistics它会按规则代码分组统计数量比如E501出现 45 次、F401出现 18 次。这样你就能快速找到性价比最高的修复方向——先把出现次数最多的那类问题批量解决告警数字会下降得非常快。4.2 分批推进每轮只动一类告警我当时的节奏是这样的你可以直接抄作业。第一批修所有 Flake8 的 F 类。这是纯逻辑问题必须人肉看。30 条左右花了一天。第二批修 Flake8 的 E/W 类风格问题。这类大多是缩进、空行、行尾空格。我写了个一次性脚本自动修能修的剩下手改。E501行太长这类不能盲目拆拆不好反而影响可读性我通常把长字符串提取成变量或者用隐式字符串拼接。第三批修 Pylint 的 E 类。和 F 类重复的跳过重点处理E0401无法导入模块。当时有一批E0401是因为代码里用了sys.path手工拼接路径导入兄弟目录模块Pylint 在纯静态分析时找不到。这个不是代码 bug但暴露了项目结构的问题。我顺手把这些路径导入改成了用相对导入既消了 lint 告警也让模块结构更清晰。第四批处理 Pylint 的 W、R、C 类。到这里已经不会全改了我的策略是W0611未使用的导入和F401重叠Flake8 已经清掉所以 Pylint 很少再报。W0612、W0613未使用的变量、参数走一遍看是不是真没用是就删。R0913参数过多这类属于“明知要改但不敢动”的我先在.pylintrc里把max-args放宽到 8然后把少数几个确实需要重构的记到技术债清单里后续排期处理。C0103命名规范直接全局 disable因为老项目里数据库字段映射的名字改起来影响面太大收益有限。每修完一批跑一次全量扫描对比数量。修复期间最少 commit 一次保证每步都能回滚——我用 Git 的 diff 检查一个原则这次提交只包含对告警的修复不夹带任何功能改动。否则两个问题搅在一起出了 bug 没法排查。4.3 用“基线”策略拦住新代码存量代码清完一波之后还有一个更重要的问题怎么防止新代码把告警数拉回去。我用的方法是baseline 基线策略。具体操作是把这次清理后的告警情况保存下来后续每次扫描只关注“新增告警”。做法很简单。假设 Pylint 全量扫描后的评分稳定在 9.0 以上就把它作为 CI 的阈值pylint myapp/ --fail-under9.0这条命令的意思是评分低于 9.0 就直接失败代码合不进去。Flake8 那边更直接flake8 myapp/ --count --max-complexity12--count会在最后打印一个总告警数。CI 里可以配合 shell 判断告警数大于 0 就非零退出。但注意这种策略有一个漏洞老项目可能还有少量历史遗留告警没清完如果阈值写死为 0新人和旧代码都会很痛苦。所以更稳妥的过渡方案是先把存量告警冻结在一个“允许列表”里后续只阻止新增。Flake8 没有原生支持 baseline 文件但社区有flake8-baseline这类工具可以做Pylint 则可以通过disable列表配合“只检查 diff 行”的 CI 插件实现。我的建议是如果团队刚起步先用“阈值”方案等告警数沉到个位数再换“允许列表”方案体验最平滑。5. 常见问题与排查技巧实录5.1 问题速查表把我在实际使用中踩过、见别人踩过的坑整理成一个速查表按“现象 - 原因 - 解决”来看即可。现象原因解决配置文件改了但没生效没有指定路径或者文件名不对Pylint 用--rcfile显式指定Flake8 支持setup.cfg、.flake8、tox.ini但项目里有多个文件时以命令行--config为准Black 格式化后 Flake8 报 E203/E501工具间默认行宽和空白规则不一致统一max-line-lengthextend-ignore E203,W503Pylint 跑得很慢没有并行、文件多、重复扫描加jobs 4persistent no或者用--files-outputyes分开输出Pylint 在 CI 里报 unable to import依赖没装全或虚拟环境路径不对在 CI 里先pip install -r requirements.txt再给 Pylint 配init-hook把项目根目录加进sys.pathFlake8 默认不检查 pyproject.tomlFlake8 原生不支持用.flake8文件代替或者装插件flake8-pyproject同一段代码本地不报、CI 报本地和 CI 版本不一致把两个工具的版本固定到requirements-dev.txt或者用 pre-commit 统一锁版本F401 和 W0611 重复报两个工具查同一件事不用管各查各的如果想省事可以在 Pylint 里 disable 掉W0611反正 Flake8 会拦想要项目忽略某个文件个别文件不适用规范Pylint 配ignoreFlake8 配exclude也可以给特定文件行尾加# noqa有一个非常容易被忽略的细节Flake8 默认不会扫描隐藏目录但如果你在.flake8里配置了exclude注意它不会自动附加默认排除项而是覆盖默认值。我踩过一次配了exclude .git,__pycache__后忘了把venv加上结果扫描把虚拟环境里几百个文件全算进去告警数直接爆表。后来我写成exclude .git,__pycache__,venv,migrations才消停。5.2 让检查变成团队习惯的最后一公里工具本身不会改变团队习惯把工具嵌进日常流程才会。我最推荐三个地方pre-commit 钩子、CI 流水线、编辑器保存时自动检查。先说 pre-commit。项目根目录建一个.pre-commit-config.yamlrepos: - repo: https://github.com/pylint-dev/pylint rev: v3.2.6 hooks: - id: pylint args: [--fail-under9.0] - repo: https://github.com/pycqa/flake8 rev: 6.1.0 hooks: - id: flake8 args: [--max-line-length100, --max-complexity12]团队成员只需要pre-commit install一次后续每次git commit都会自动跑检查跑不过就不让提交。相比 CI 在推送后才报错这种方式把发现问题的成本提前到了本地体验好太多。再说 CI。我自己常用的 GitHub Actions 长这样- name: Lint with Pylint and Flake8 run: | pip install pylint flake8 pylint myapp/ --fail-under9.0 flake8 myapp/ --countGitLab CI 里也差不多核心就fail-under和--count这两个参数。CI 的价值是兜底防止有人本地绕过 pre-commit我见过直接把钩子卸载的保证主分支上的代码永远符合最低标准。最后是编辑器集成。VS Code 里装 Python 扩展后跑一条命令python.linting.pylintEnabled: true, python.linting.flake8Enabled: true, python.linting.lintOnSave: true保存即检查问题直接在底部面板里列出来红色波浪线指到具体位置。这个交互反馈比任何文档都管用新人进团队的时候被编辑器里的红线“教”几次自然就学会了规范。结尾最后说点个人的大实话。在团队里推 lint 工具最难的不是配置而是让大家接受“被机器挑刺”这件事。我做过最有效的一件事不是发规范文档也不是写自动化脚本——而是在周会上演示了一次把 Flake8 报出来的几个 F 类告警对应回线上真实出现过的故障让所有人看到“这些检查不是形式主义是真的能在故障发生前拦住问题”。从那以后团队对新告警的态度从“烦”变成了“感谢”。几年下来我的体会是工具的价值从来不是把评分从 3.9 刷到 10而是让没必要的低级错误根本进不了代码库让人的精力能集中在真正值得花时间的重构和设计上。如果你刚开始在自己的项目上配置这两个工具就先从max-line-length 100、默认规则全开、Flake8 改到 0 告警开始这已经能拦住相当大比例的“看起来没问题”。剩下的等真正遇到了问题再慢慢调。
返回列表