简介:Pychecker 0.8.19 是一款面向 Python 开发者的静态代码分析工具,用于在程序运行前自动扫描源代码,发现类型错误、未定义引用、未使用变量、异常处理不规范等常见问题,并能对函数与类规模、循环依赖、复杂表达式等给出提示,适合希望在开发阶段快速提升代码质量的个人或团队。压缩包为 rar 格式,共 54 个文件,以 41 个 py 源文件为主体,涵盖工具核心模块与测试用例;剩余包括 readme、changelog、KNOWN_BUGS 及配置文件等说明文档,便于了解版本变更与已知限制。整体体积仅 176KB,轻量易部署。已有 391 人浏览学习。借助本包,读者可获取完整可运行的 pychecker 工具,在命令行中对单个脚本或整个目录进行检查,并结合自带测试用例理解各类检查规则的用法;定期运行此类检查可帮助减少后期调试成本,尤其适合中大型项目或多人协作场景。
1. pychecker工具到底解决了什么问题
如果你翻看过早期Python项目的CI脚本,一定见过pychecker这个不起眼的命令行工具。它不像pylint那样管天管地,也不像mypy那样做类型推导,却能在零配置下几秒钟扫完一大片代码,把未使用变量、可疑导入和永远走不到的分支一条条列出来。在动态语言普遍靠运行时爆炸兜底的年代,pychecker用静态分析的笨办法,提前堵住了不少低级错误。我会从它还能不能打、怎么跑通、怎么接进项目,以及那些让工具翻车的坑,一步步拆开讲。适合手里有遗留Python项目、又不想为代码检查引入一套重型体系的开发者。
2. 为什么还要用pychecker:与pylint、pyflakes、mypy的边界
2.1 pychecker的检查原理:AST分析、import跟踪和警告体系
pychecker的检查过程其实不神秘。它先把源码读进来,用Python内置的parser生成抽象语法树(AST),然后对不同类型的AST节点做模式匹配。所谓模式匹配,就是写死了一些规则:比如遇到Return语句,就检查返回表达式是否和函数声明的返回注解兼容;遇到Import,就尝试在搜索路径里解析模块;遇到赋值,就把变量名记进符号表。做完这些之后,它把符号表里的定义和使用做一次对账:定义过但从未读取的变量、读取了但从未定义的名称、导入了但没有任何引用的名字,都会变成警告。
这个对账过程可以类比编译器前端的符号表分析:先构建一张表,记录每个作用域里有哪些变量被定义、哪些函数被声明;然后在每个表达式中寻找名称引用,把引用与定义关联起来。如果某个定义在离开作用域时没有任何引用指向它,就报告未使用变量。如果某个引用找不到任何对应的定义,就报告未定义名称。import检查也遵循类似的逻辑,只不过额外要做的,是在搜索路径中尝试加载模块,并解析模块内部是否有你导入的属性。
这种做法的优势在于,它不执行代码,因此不会有副作用,也不会因为你写的某个分支需要用户输入就跑不到;缺点同样明显,所有动态特征——eval、exec、getattr、通过循环拼接属性名——在它眼里都是一团乱麻。理解了这一点,你就能预判哪些文件会误报。
pychecker的警告体系粗略分三级:错误(例如函数参数个数不匹配)、警告(例如未使用变量)、风格提示(例如冗余括号)。默认它会全部输出,但不同类别在输出时会有一行前缀,方便你用工具过滤。相比pyflakes,pychecker还会检查跨函数的信息,比如调用一个函数时传了三个参数,而这个函数只定义两个,pyflakes不会说话,pychecker会直接警告。这听起来是小事,但在重构接口时,这种检查能提前截住一批TypeError。
2.2 现代工具做不到的轻量场景:无配置、单命令、快速反馈
现在主流工具里,pylint是最常被推荐的,但它的默认配置对遗留代码极不友好:一条未定义变量和一条命名风格问题混在一起,几十个文件扫下来,真正要修的反而被淹没。mypy则需要你先给代码补类型注解,这在老项目上是几乎不可能完成的任务。pyflakes与pychecker最接近,但它只做语法层面的分析,不会检查函数调用参数数目、类继承时的方法签名这类跨函数的信息。
所以我的排序是:新项目用mypy做主、pylint做辅助;遗留项目如果只想快速知道有没有必炸的问题,pychecker反而是更好的第一道筛子。比如一个几千行的老模块,跑pylint可能出现几百条风格警告,但跑pychecker可能只有几条真正会导致崩溃的错误。先用pychecker把这几条修掉,再决定要不要深入做代码规范化,是一种很划算的步骤。
这种工作流在今天依然有价值,尤其是在构建环境里只有系统Python、装不了第三方包、网络受限的场合。pychecker是纯Python脚本,拷一个文件过去就能跑,不需要编译,不需要下载依赖。某次我在一个隔离网络环境里做代码扫描,那台机器连不上包仓库,能用的只有系统自带的Python和几个标准库包。pylint和mypy都没法安装,但pychecker作为单个脚本文件,直接拷过去就可以跑。这种能力在今天看起来不起眼,但在隔离环境里非常救命。
2.3 对比表格与选型建议
下面是我在项目里常用的一套选型对比,表里不是要分高下,而是帮你判断哪个工具该放在流水线的哪个位置。
| 工具 | 检查重点 | 配置成本 | 运行速度 | 适用场景 |
|---|---|---|---|---|
| pychecker | 未使用变量、导入错误、参数/类型不匹配 | 极低,命令直接跑 | 快 | 遗留代码快速排雷、受限环境 |
| pyflakes | 未使用变量、未定义名称 | 极低 | 很快 | 语法层快速检查 |
| pylint | 风格、错误、复杂度等上百条规则 | 高,需要配置文件 | 中 | 团队规范统一、代码评审 |
| mypy | 类型注解推导与校验 | 中,需要渐进式注解 | 慢 | 需要类型安全的新项目 |
通常我会在CI里同时放两个阶段:第一阶段跑pychecker或pyflakes,确保没有低级的未定义名称和未使用变量;第二阶段跑pylint,执行团队规范。这样分工,既不会让构建被风格问题卡住,也不会让真正的错误被风格问题淹没。
当然,pychecker对Python 3语法支持是它的软肋,这个我会在避坑章节展开。选型时要先确认你手上的代码库能否被它正确解析。
2.4 什么时候不该用pychecker
如果你在做一个全新的Python 3项目,我会直接劝退pychecker。新代码没有历史包袱,直接用mypy做类型检查、pylint做风格检查,得到的收益远大于pychecker。另外,如果你的团队已经强制一套严格的代码规范,比如所有函数必须有类型注解、所有变量必须满足命名规则,那么pychecker那套简单的警告体系覆盖不了这些诉求,硬接进来只会增加一份需要维护的例外清单。
还有一个场景是脚本型项目,比如只包含几个独立.py文件的自动化脚本。这种项目通常不存在跨模块问题,pyflakes就足够了,pychecker的价值反而体现不出来。选型不是越强越好,而是让工具刚好在它擅长的区域里干活。
3. 本地跑通pychecker:安装、最小命令和第一个警告
3.1 安装与最小命令
先解决安装。以PyPI上的包为例,常见做法是:
# 在Python 2环境下安装 pip install pychecker # 在Python 3环境下尝试安装,如果报语法错误,则不要强求 pip install pychecker我实际遇到的情况是,在Python 3.8以上版本里,pip安装后运行直接抛SyntaxError。所以如果你正在用Python 3,建议先在一个Python 2虚拟环境里做检查。另一种常见做法是从某些发行版的旧软件源里安装pychecker包,它会被拉进系统的Python 2运行时。
如果你手头有Python 2.7,可以这样创建虚拟环境:
python2 -m virtualenv venv2 venv2/bin/pip install pychecker如果你的机器上连Python 2都没有,可以找一个带有Python 2的旧容器镜像临时用。把这一步做好,后续所有pychecker相关操作都在这个环境里进行,避免主环境的解释器兼容问题。
安装完验证一下:
pychecker --version如果输出了版本号,说明工具可用。接下来是最小命令:
pychecker demo.py它会读取demo.py并输出所有警告。目录下多个文件可以用通配符:
pychecker *.py如果你不想让命令找不到模块,把项目根目录加进去:
pychecker --search-path=. mymodule.py这里--search-path的作用是告诉pychecker去哪里找import的模块,相当于设置了Python的sys.path。如果省略它,工具只会检查你传入的文件,不会主动跟着import去加载依赖。对于跨模块的代码库,这个参数几乎必加。
3.2 看懂警告输出:类别、行号和解决顺序
我第一次跑pychecker时被它的输出风格愣了一下:不是pylint那种带规则名和链接的格式,而是非常朴素的“文件名:行号: 消息”。比如检查下面这段代码:
# demo.py def add(a, b): result = a + b return apychecker会报告两个问题:第2行的result赋值后没有被读取;第3行返回a却没有用到b。这种输出非常直接,说明变量的定义和使用对账失败。
拿到第一个警告后,不要急着改代码。先根据警告类型判断是定义没用到,还是定义找不到。前者是删除或换成下划线前缀,后者往往是拼写错误或遗漏import。pychecker的警告消息非常直白,比如“Variable (result) has no visible usage”这类描述,你一眼就知道问题在哪。
解决顺序上,我一般先用--only=error筛出真正的错误级问题,比如函数参数个数不匹配、未定义名称。这些问题一旦漏到线上,运行时会直接抛异常。未使用变量属于warning级,即使不修也不会立刻出事故,可以集中到代码清理阶段一把处理。风格提示更是放到最后,甚至可以完全忽略。
3.3 必调参数:--only、--limit、--ignore的正确打开方式
默认输出可能很多。我一般会用三个参数控制输出量。
# 只显示错误(如果支持按类别过滤) pychecker --only=error demo.py # 限制最多显示30条,防止刷屏 pychecker --limit=30 demo.py # 忽略特定文件或检查器(以pychecker --help为准) pychecker --ignore=some_checker demo.py注意,不同版本的pychecker对参数的支持略有差异,我的习惯是先跑pychecker --help,把所有参数列出来看一眼再写进脚本。特别重要的是--ignore后面的检查器名称,不同版本拼写可能不同,抄一条旧配置很可能在新版本里直接失效。
还有一个很实用的参数是--search-path,当你的代码import了本地包,但这个包不在默认路径里时,会大量误报“找不到模块”:
pychecker --search-path=src:lib demo.py这里用冒号分隔多个路径,Windows下需要把冒号换成系统支持的分隔符(通常是分号)。如果你不想让输出里混入第三方依赖的警告,常见做法是直接把第三方目录排除在命令之外,而不是把整个venv加进搜索路径。
可以把多个参数组合起来:
pychecker --limit=50 --only=error --search-path=src mymodule.py这样只会输出前50条真正的错误级问题,适合在CI里当作阻断条件。未使用变量和风格提示则放到本地开发时再看。
还有一个容易踩的细节:--ignore并不是万能的,它通常会关闭某一个检查器,而不是某一条具体警告。如果你想保留这个检查器而跳过某类误报,只能靠文件排除。这也是为什么很多项目会在脚本里维护一个可忽略文件列表,而不是依赖工具参数。
4. 把pychecker接进项目:配置、假阳性过滤和CI集成
4.1 用shell脚本封装常用参数
每次敲一长串参数容易漏,我习惯在项目根目录放一个check.sh,把pychecker的调用固定下来。
#!/bin/bash set -euo pipefail DEFAULT_FILES="$(find . -name '*.py' -not -path './venv/*' -not -path './.env/*')" FILES="${@:-$DEFAULT_FILES}" if [ -z "$FILES" ]; then echo "没有找到待检查的Python文件" exit 0 fi pychecker --limit=50 --search-path=src $FILES这段脚本的核心是:默认扫整个项目,但排除虚拟环境目录;允许在命令行手动传入特定文件。set -euo pipefail能让脚本在任何异常时退出,避免CI误以为成功。如果你把脚本放到了项目根目录,记得先chmod +x check.sh。
不要把这个脚本写死成Linux专用。如果你有同事在Windows上跑,可以加一个PYTHON和WC的命令变量,或者直接用python调用pychecker.py。我通常会在开头加一个OSTYPE判断,但这部分留给读者按团队环境自己补。关键是让所有人在同一个入口上提交,而不是各敲各的。
4.2 只检查变更文件:用git diff过滤
全量检查在大型项目上耗时且容易刷屏。更实用的做法是只在pre-commit或CI里检查刚改动的文件。
# 获取本次改动的Python文件 changed=$(git diff --name-only --diff-filter=MA -- '*.py') if [ -n "$changed" ]; then pychecker --search-path=src $changed else echo "没有Python文件变动,跳过检查" fi注意--diff-filter=MA是只保留修改和新增文件,删除的文件就不需要检查了。这样每次提交只用跑到几个文件,反馈速度快很多。
但这也带来一个盲区:如果有一个未使用变量在很久以前引入,直到今天才被某个改动暴露,增量检查不会发现存量问题。所以在CI里我一般会同时跑一次全量,只是把全量的时间窗口放到夜间流水线。我的习惯是:pre-commit阶段只跑增量,保证反馈快;每天深夜的全量检查把存量问题暴露出来,防止技术债偷偷生长。
4.3 用基线文件让CI只报告新增问题
全量检查通过的前提,是存量警告已经清理到零。如果你的项目历史包袱重,可以先允许存量警告存在,但在后续合并请求中不允许新增警告。做法是把某一天的全量输出保存为baseline.txt,之后的每次CI都对比当前输出,只把新增的当作失败。
# 生成基线(在干净的主干分支上执行) pychecker --search-path=src . > baseline.txt # 在CI中对比 pychecker --search-path=src . > current.txt # 忽略行号差异,只关心警告是否真的新增 awk -F: '{ $2=""; print }' baseline.txt | sort | uniq > baseline.sig awk -F: '{ $2=""; print }' current.txt | sort | uniq > current.sig if comm -13 baseline.sig current.sig | grep .; then echo "有新增pychecker警告,请修复后再提交" exit 1 fi这个脚本的关键在awk,它把第二列(行号)去掉,只保留文件名和消息。这样增加空行导致的行号漂移就不会造成假阳性。注意comm -13的输出是第二个文件独有的行,也就是新增警告。如果某个新增警告确实是误报,可以在脚本里维护一个允许列表,用grep -v把这些行过滤掉,而不是去修改工具参数。
基线文件不是永久有效的。三个月后如果团队已经把存量警告清掉一半,基线应该重新生成,否则所有新警告都会和旧警告混在一起,对比失去意义。我通常在每个版本迭代开始时重新生成基线,并把基线文件签入代码仓库。
4.4 与pre-commit钩子结合
如果你在使用pre-commit,可以在.pre-commit-config.yaml里加一个本地钩子,让每次提交前自动跑脚本。
repos: - repo: local hooks: - id: pychecker name: pychecker entry: bash check.sh language: system files: \.py$关键点是language必须写成system,entry指向我们前面写的check.sh。pre-commit会在提交文件的列表中过滤出Python文件,然后把它们作为参数传给脚本,正好配合check.sh里的$@。
注意pre-commit默认会把待提交的文件名传给entry,如果你的check.sh没有接收参数,可以用pass_filenames: false关闭,但这意味着脚本会检查所有文件,而不是只检查本次提交的文件。我自己一般直接用默认传参。另外,pre-commit通常不会设置与CI相同的PYTHONPATH,所以脚本里的--search-path一定要写绝对路径或项目相对路径,否则本地能过,CI上就可能误报。
5. pychecker避坑实录:5个让工具失效的典型场景
5.1 Python 3环境下安装失败或启动报错
现象:在Python 3环境中pip install pychecker之后,执行pychecker --version直接抛SyntaxError,或者提示找不到文件。
原因:工具主体是基于Python 2语法写的,多年没有更新,部分源码在Python 3下无法解析。
解决:不要和解释器硬刚,单独建一个Python 2虚拟环境,在里面安装并运行。如果你的团队已经完全不保留Python 2运行时,就别再执着,直接用pyflakes代替,检查能力覆盖后面几种常见场景。当时我在一个迁移项目里,为了保留CI脚本,专门用容器跑了一个Python 2环境,每次打包时单独执行pychecker。虽然麻烦,但这样可以精确对比迁移前后是否出现新问题。容器标签选择带python2字样的基础镜像,启动之后直接装包,检查完就退出。这个流程并不复杂,但需要你在项目文档里写清楚,否则三个月后没人记得为什么CI里跑着一个古老解释器。
5.2 虚拟环境下大量“未定义模块”误报
现象:代码import了已安装到venv里的包,pychecker仍然报“找不到模块”,导致输出里全是误报。
原因:pychecker默认搜索路径是启动时解释器的sys.path,而虚拟环境的site-packages不一定在这个范围里。
解决:在命令里加上--search-path,填上你的venv路径。或者更稳妥的办法是,直接用项目根目录下的系统Python运行pychecker,避免虚拟环境带来的路径差异。判断方法很简单:在同样的Python环境中启动python解释器,执行import这个模块,如果能成功,说明路径没问题;如果也报ImportError,说明包没装对。不要一上来就加--search-path,先把包装好。
要注意的是,--search-path如果加错了位置,可能让pychecker误把所有本地代码都当成可导入的模块,反而掩盖了真正缺失的依赖。所以路径粒度要按src或lib来加,不要只加一个根目录。
5.3 动态代码导致误报:eval、exec、反射调用
现象:你的代码用getattr(obj, method_name)动态调用方法,pychecker报“对象没有该属性”;用exec拼一段代码赋值,pychecker直接报语法警告。
原因:静态分析器只能看到模板字符串,无法推断运行时的属性名。
解决:把这类模块加入排除名单,在脚本里用一个数组维护需要跳过pychecker的文件,确保CI不会因为反射调用卡死。这里没有特别好的根治方案。如果误报比例太高,说明这段代码本身的可读性已经出问题了,比清理pychecker更值得做的是重构掉这些动态调用。有一种临时方案是在文件顶部写一个注释标记,然后在脚本里用grep过滤这个标记对应的警告。虽然丑,但在遗留代码里很实用。后来我干脆把所有反射调用集中到一个模块里,让pychecker忽略整个模块,其他地方不再出现动态调用。
5.4 基线文件因行号漂移而失效
现象:执行基线对比时,明明没有新增警告,但diff却提示大量差异。
原因:pychecker输出包含行号,而你只需要按“文件+警告消息”判断是否新增。
解决:在对比前用awk去掉行号列,再排序比较,这个技巧在4.3已经给过。但要注意,如果文件被重命名、函数被移动,基于路径的签名也会失真。更稳妥的做法是按消息内容分桶,而不是按明细行逐条对比。如果团队没有能力定期清理存量警告,基线法很容易变成纸面功夫。我见过有人把基线文件越滚越大,最后CI变成摆设。所以基线法必须搭配一个告警数量上限,一旦超过就强制全队停手清理。从长远看,与其做行号忽略,不如把pychecker替换成支持“警告唯一ID”的工具,像pylint和ruff那样,每条警告都有一个稳定ID。这样基线对比就不会受到行号影响。pychecker本身没有这套机制,所以基线法只是过渡方案。
5.5 编码问题导致中文注释直接报错
现象:包含中文注释的.py文件在Windows或旧Linux环境下运行pychecker时,抛出UnicodeDecodeError。
原因:pychecker读取文件时使用系统默认编码,而项目文件是UTF-8;读取字节流后转为Unicode失败。
解决:在文件头加上# -*- coding: utf-8 -*-声明,或者在脚本里先转换文件到UTF-8再检查。更简单的是把PYTHONIOENCODING环境变量设为utf-8再运行pychecker。这条在大型遗留项目上特别常见,因为历史文件编码五花八门。与其逐个文件加声明,我通常是在CI容器里统一设置环境变量LANG=C.UTF-8,这样pychecker启动时的文件编码就是UTF-8。在CI里,把LANG和PYTHONIOENCODING都设置成UTF-8,其他工具也不会因为中文代码而翻车。设置方式在每一个CI平台上略有不同,但核心就是两个环境变量。
6. 把pychecker当骨架用:自建轻量静态检查器的思路
讲这么多坑,其实核心建议是:别把pychecker当成不可替代的终态工具,而是把它当成一个可以拆卸的骨架。它的AST检查思路,完全可以移植到你自己的小工具里。我自己写过几十行的检查脚本,专门用来盯团队自己约定的规则,比如禁止裸except、禁止在循环里import,这类规则在pychecker没有现成选项,但用Python标准库ast模块写起来非常简单。
一个最小例子,检查裸except:
import ast import sys class BareExceptFinder(ast.NodeVisitor): def __init__(self): self.violations = [] def visit_ExceptHandler(self, node): if node.type is None: self.violations.append((node.lineno, "禁止裸except")) self.generic_visit(node) if __name__ == "__main__": for filename in sys.argv[1:]: with open(filename, encoding="utf-8") as f: tree = ast.parse(f.read(), filename=filename) finder = BareExceptFinder() finder.visit(tree) for lineno, msg in finder.violations: print(f"{filename}:{lineno}: {msg}")逻辑很简单:ast.parse把源码变成AST,NodeVisitor会遍历所有ExceptHandler节点,如果type字段是None,就说明except后没有跟异常类型,也就是裸except。这个检查器不会受pychecker版本影响,也不依赖任何第三方库,只要你的项目用Python 3.8以上就能一直跑。
同样的思路可以扩展出很多规则:检查魔法值直接出现在条件里、检查禁止的API调用、检查模板字符串里是否带了HTML片段……你只需要写一个visit_对应节点名的方法,然后把违规行收集起来。我后来在一份旧项目里就是用这种小脚本替代了大部分pylint的团队规范检查,因为团队规范每年都在变,与其去翻pylint的插件文档,不如直接在代码库里放一个300行的check_rules.py,谁都能看懂,改起来也快。
血泪教训是:自定义脚本一定要把输出格式对齐pychecker的“文件名:行号: 消息”,这样你的CI日志、IDE正则匹配、同事的肌肉记忆都能无缝衔接。我最初贪图省事用纯文本,结果每次增量检查都要额外写一个解析器,后来还是乖乖改回了统一格式。希望这个思路能帮到你。如果你也维护着上了年纪的Python代码库,先别急着给全部文件标注释,做个几十行的AST检查器,可能比在旧工具上打补丁更值得投入。
本文还有配套的精品资源,点击获取