使用 pstats 分析 Python 剖析统计:Stats、排序键、过滤器与交互式命令行完全指南
导读:
pstats是 CPython 标准库中专门用于读取、合并、排序、过滤与展示剖析(profiling)结果的模块。它既能解析确定性追踪剖析器(profiling.tracing,兼容cProfile)的输出,也支持统计采样剖析器(profiling.sampling)生成的数据。读完本文,你将掌握pstats.Stats的全部核心 API、14 种排序键及其选择策略、三类过滤限制的精确用法、多份剖析文件的合并技巧,以及python -m pstats交互式浏览器的每一个命令。
本文以仓库中的官方文档 Doc/library/pstats.rst 为主体,结合其完整实现 Lib/pstats.py(共 840 行)与单元测试 Lib/test/test_pstats.py 展开源码级讲解。
pstats 在 Python 性能剖析体系中的位置
现代 CPython 中,性能剖析工具被组织为一个完整流水线:先由剖析器采集数据写入文件,再由 pstats 负责后续所有"读、算、排、筛、印"环节。这一点可以从本仓库的结构看出:
- 采集端:确定性(追踪式)剖析器位于
profiling.tracing子包,底层由 C 扩展_lsprof驱动;统计采样式剖析器位于profiling.sampling子包。两者介绍见 Doc/library/profiling.rst、Doc/library/profiling.tracing.rst 与 Doc/library/profiling.sampling.rst。 - 分析端:即本文主角
pstats,实现文件为 Lib/pstats.py,模块公开接口为Stats、SortKey、FunctionProfile、StatsProfile(见 Lib/pstats.py 的__all__)。
官方文档在"参见(see also)"一节中把 pstats 定义为三条配套文档的公共下游:profiling 提供整体概览,profiling.tracing 是确定性追踪剖析器,profiling.sampling 是统计采样剖析器。也就是说,无论你用哪一端产出数据文件,都可以用同一个 pstats 工具链完成分析。
需要特别说明的是:剖析数据文件格式只对生成它的 Python 版本有效。官方文档明确写道,不同 Python 版本之间、不同剖析器之间不存在格式兼容性保证。原因可以从实现找到——Stats.load_stats 和 dump_stats 直接使用 marshal 序列化统计字典(Lib/pstats.py),而 marshal 格式本身就与解释器版本绑定。
快速上手:加载文件并打印报告
最基本的用法只需两行:
import pstats
p = pstats.Stats('profile_output.prof')
p.print_stats()
如果想直接看累计耗时最高(cumulative time)的前 10 个函数——这是性能分析最高频的诉求之一——则先排序再截取:
from pstats import SortKey
p = pstats.Stats('profile_output.prof')
p.sort_stats(SortKey.CUMULATIVE).print_stats(10)
print_stats(10) 中的 10 是"限制(restriction)",含义是只输出排序后前 10 条。Stats 对象的设计天然支持方法链,例如官方文档给出的惯用法:
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()
这里 strip_dirs() 去除文件名中的目录前缀让输出更紧凑,sort_stats(-1) 是旧式数值参数,等价于 'stdname'(详见下文排序一节)。所有修改方法都返回 self,这正是 Lib/pstats.py 类注释中示例链式调用得以成立的原因。
Stats 类的完整 API 剖析
构造:Stats(*filenames_or_profile, stream=sys.stdout)
Stats 的构造参数可以是文件名(字符串或 path-like 对象),也可以是剖析器对象(如 profiling.tracing.Profile 的实例)。同时给出多个来源时,它们的统计会被自动合并。
构造逻辑对应源码 Lib/pstats.py:第一个参数交给 init() 加载,其余参数全部交给 add() 追加合并。init() 内部会完成全量统计(total_calls 总调用数、prim_calls 原始调用数、total_tt 总耗时)与顶层函数识别,并维护用于对齐输出的最长函数名字节数 max_name_len。
stream 参数决定 print_stats 及其同类方法把报告写到哪个输出流,默认为 sys.stdout。这在测试与嵌入式报告中非常有用:单元测试 Lib/test/test_pstats.py 就用 StringIO() 充当流来静默接收全部输出。你也可以用同样的手法把报告捕获进字符串再做后续处理。
在 Lib/pstats.py 的 load_stats 中可以看到一个重要的自动分派逻辑:如果文件内容里含 ('__sampled__',) 标记,说明这是统计采样剖析器生成的数据,pstats 会弹出该标记并把对象类切换为 SampledStats(采样统计专用子类),从而让表头、排序键和含义整体切换为"样本(sample)"语义。这印证了文档所述"pstats 同时支持两类剖析器输出"。
strip_dirs():压缩文件名
strip_dirs() 把所有文件名中前导目录信息去掉(保留 basename)。它是就地修改并返回 self 以便链式调用。源码 Lib/pstats.py 展示了两个值得注意的副作用:
- 目录被剥离后,函数之间的调用者/被调用者关系会一并重算(调用者的文件名同样被 strip,
func_strip_path使用os.path.basename)。 - 若不同目录下出现"同名文件、同行号、同函数名"的两个条目,它们会通过
add_func_stats被合并为同一条统计。 - 文档特别提示:执行完
strip_dirs()后,若尚未sort_stats,统计数据处于"随机顺序"状态——源码中strip_dirs()会把fcn_list置None,而打印时的排序列表正是由sort_stats()生成的fcn_list。
add(*filenames):增量合并剖析数据
add() 可以从更多文件追加剖析数据,官方文档指出这些文件必须由同一类剖析器产生;来自相同函数(文件、行号、函数名三者一致)的统计会被累加。实现上 add()(Lib/pstats.py)不仅逐条目调用 add_func_stats 求和 (cc, nc, tt, ct) 四元组,还会用 add_callers 把双方的调用者字典逐项相加(元组格式逐位相加、旧式计数格式直接加和),同时累计总调用数与总耗时。
dump_stats(filename):把当前统计写回磁盘
dump_stats 将当前 self.stats 字典以 marshal.dump 序列化保存(Lib/pstats.py):文件不存在则创建,已存在则覆盖。保存结果可以用 Stats(filename) 重新载入。测试 Lib/test/test_pstats.py 验证了"dump 后再 load 得到的 stats 字典与原对象完全相等"。这一能力用于剖析阶段的"采集与分析分离":线上采集文件,事后离线分析。
sort_stats(*keys):排序与全部排序键
sort_stats 接受一个或多个键,每个键可以是字符串,也可以是 SortKey 枚举成员;传入多个键时,靠后的键用于打破靠前键的平局(次排序键)。官方文档建议优先使用 SortKey 枚举,因为相比字符串它提供更好的错误检查(编译器/编辑器层面即可拦截拼写错误)。
完整排序键对照表(官方文档原表,逐项继承):
| 字符串写法 | 枚举成员 | 含义 |
|---|---|---|
'calls' |
SortKey.CALLS |
调用次数 |
'cumulative' |
SortKey.CUMULATIVE |
累计时间(含子调用) |
'cumtime' |
无 | 累计时间(同上) |
'file' |
无 | 文件名 |
'filename' |
SortKey.FILENAME |
文件名 |
'module' |
无 | 文件名(同上) |
'ncalls' |
无 | 调用次数(同上) |
'pcalls' |
SortKey.PCALLS |
原始(非递归)调用次数 |
'line' |
SortKey.LINE |
行号 |
'name' |
SortKey.NAME |
函数名 |
'nfl' |
SortKey.NFL |
函数名/文件名/行号 |
'stdname' |
SortKey.STDNAME |
标准名 |
'time' |
SortKey.TIME |
内部时间(不含子调用) |
'tottime' |
无 | 内部时间(同上) |
源码层面的对照关系清晰可见:枚举定义位于 Lib/pstats.py,其中多个枚举成员同时注册了别名值(如 CALLS 同时对应 'calls' 与 'ncalls',TIME 对应 'time' 与 'tottime',CUMULATIVE 对应 'cumulative' 与 'cumtime',FILENAME 对应 'filename' 与 'module'),因此表格中那些"N/A"的字符串与对应枚举是等价键。
排序方向规则:所有基于耗时的排序都是降序(耗时最长的排最前),而基于名称、文件、行号的排序是升序(字母序)。这一规则由 Lib/pstats.py 的 sort_arg_dict_default 中每个键的方向标记(-1 降序 / 1 升序)决定,最终由 TupleComp.compare(Lib/pstats.py)执行多级比较。
关于 NFL 与 STDNAME 的差异:两者都按"名称→文件→行号"排序,但 NFL 把行号当作数值比较,而 STDNAME 按整个标准名字符串 "文件:行号(函数名)" 做字符串比较。此外,sort_stats(SortKey.NFL) 与 sort_stats(SortKey.NAME, SortKey.FILENAME, SortKey.LINE) 完全等价。
兼容旧版 profile 的数值参数:为保证向后兼容,-1、0、1、2 四个整数仍被接受,分别对应 'stdname'、'calls'、'time'、'cumulative'。该映射直接写在 Lib/pstats.py,并由测试 Lib/test/test_pstats.py 逐项验证。
唯一的缩写前缀自动补全:get_sort_arg_defs()(Lib/pstats.py)会把每个合法键按"逐字缩短前缀"展开注册(如 'c'→'calls'、'f'→'filename'),前提是该前缀不会产生歧义;若有歧义则整段前缀被剔除。这解释了交互式浏览器里 sort 命令支持"唯一前缀"的机制。注意:即使只用字符串键,sort_stats 也会校验键的合法性与参数类型一致性——混用字符串与枚举(如 'calls' 与 SortKey.TIME)会抛出 TypeError,这一约束被测试 Lib/test/test_pstats.py 明确覆盖。
reverse_order():反转排序方向
reverse_order() 就地反转当前排序方向并返回 self。默认方向已按排序键自动选择(时间类降序、名称类升序),此方法用于产生相反视图。实现位于 Lib/pstats.py:直接把 fcn_list 反转。
print_stats(*restrictions):打印统计报告
print_stats 输出报告。从源码(Lib/pstats.py)可以看到报告头部由以下几行组成:
- 数据来源文件名(含文件修改时间)列表;
- 被判定为"顶层入口"的函数名列表;
- 汇总行:
总函数调用数 in 总耗时秒,当总调用数与原始(非递归)调用数不等时还会额外标注(N primitive calls); - 随后是按最后一次
sort_stats排序的函数统计表。
表体标题行固定为(见 print_title,Lib/pstats.py):
ncalls tottime percall cumtime percall filename:lineno(function)
各列含义与剖析内部模型一一对应。Stats 内部每条记录是五元组 (cc, nc, tt, ct, callers):cc 为原始(非递归)调用次数,nc 为含递归的全部调用次数,tt 为函数自身内部耗时(inlinetime),ct 为含子调用的累计耗时(totaltime)。当 nc != cc 时,ncalls 列显示为 nc/cc 的形式(见 print_line,Lib/pstats.py)。该五元组结构在追踪剖析器端由 Lib/profiling/tracing/init.py 的 snapshot_stats 组装,cc = nc - reccallcount 正是对递归调用的剔除。
限制(restriction)的三种类型(由 Lib/pstats.py 的 eval_print_amount 实现):
| 限制类型 | 行为 | 说明 |
|---|---|---|
整数 int |
只输出前 N 条 | 例如 print_stats(10) 输出前 10 条 |
浮点数 0.0 ≤ x < 1.0 |
输出前 x% 条 | 例如 print_stats(.1) 输出前 10% |
| 字符串 | 正则表达式过滤 | 对函数"标准名"执行 regex.search,命中才保留 |
多个限制按顺序依次施加。官方文档示例:
# 先截取前 10%,再过滤出名字里含 "init" 的函数
p.print_stats(.1, 'init')
# 先按文件名排序,再匹配含 "foo:" 的文件,最后截取前 50%
p.sort_stats(SortKey.FILENAME).print_stats('foo:', .5)
关于字符串匹配的底层细节值得注意:eval_print_amount 是把正则表达式作用于 func_std_string(func) 返回的完整标准名(形如 文件:行号(函数名),内置函数形如 {...}),而不仅是函数名本身。因此 'init' 这类子串可以命中路径或模块名中的任意一段。若正则不合法,报告会附加 <Invalid regular expression ...> 提示而不会崩溃。
print_callers(*restrictions) 与 print_callees(*restrictions):调用关系视图
print_callers 展示"每个被展示函数是被谁调用的";print_callees 是它的逆视图,展示"每个被展示函数调用了谁"。两者接受与 print_stats 完全相同的限制参数。
调用者输出时表头为 Function was called by...,被调用者视图表头为 Function called...(见 Lib/pstats.py)。对 profiling.tracing(即 cProfile 兼容路径)生成的数据,每个调用者行会给出三个数字:该调用者发起的调用次数、这批调用自身的 tottime 与 cumtime。当 ncalls 与原始调用数不同时会以 nc/cc 显示。这三种数字仅对"新式"调用者格式(元组类型)存在——print_call_heading 会检测首个调用者值是否为元组来决定是否打印子表头 ncalls tottime cumtime(Lib/pstats.py)。
从实现角度,调用者关系直接取自每条记录的 callers 字典;print_callees 需要先把所有记录的 callers 关系"反查"并缓存为 all_callees,这一倒排计算由 calc_callees(Lib/pstats.py)惰性完成(首次调用才计算,之后复用)。
get_stats_profile():程序化访问统计
get_stats_profile()(自 Python 3.9 加入,见 Lib/pstats.py)返回一个 StatsProfile 对象,供**程序化(非文本)**方式消费剖析数据,典型用途是构建自定义报表或接入可视化前端。
两个新增公开数据结构定义在同文件顶部:
StatsProfile:包含total_tt(总内部耗时)与func_profiles——一个"函数名 →FunctionProfile"的字典(Lib/pstats.py);FunctionProfile:一个@dataclass,字段为ncalls、tottime、percall_tottime、cumtime、percall_cumtime、file_name、line_number(Lib/pstats.py)。
注意 ncalls 字段在存在递归时是形如 "120/100" 的字符串(nc/cc),而 percall 时间在调用次数为 0 时取 -1 作哨兵值;所有时间值都经 f8(保留 3 位小数)舍入成浮点数。若当前没有可用的函数列表则返回空的 StatsProfile(0, {})。单元测试 Lib/test/test_pstats.py 演示了标准用法:用 cProfile.Profile 记录三个空函数调用,再断言 func_profiles 中确实包含 pass1/pass2/pass3。
不同剖析器数据的输出差异:SampledStats
pstats 对统计采样剖析器数据做了专门适配。当读入带 ('__sampled__',) 标记的文件后,实例会自动切换为 SampledStats 子类(Lib/pstats.py),此时:
- 排序键体系替换为采样语义:
samples/nsamples(样本计数)、psamples、以及同样可用的cumtime/filename/line/name/nfl/stdname/time等; - 表头改为
nsamples tottime persample cumtime persample; - 调用者子表头也相应变为
nsamples tottime cumtime。
也就是说,同一个 Stats 打印管线在检测到采样数据后自动切换了"量纲",阅读报告时不会再出现把采样数误当调用次数的混淆。
排序与过滤的实际选择建议
综合排序键语义,可归纳出针对不同问题的选键策略(均可直接复制运行):
from pstats import SortKey
# 想找"谁最该优化":看累计耗时,找出自身+子孙调用最耗时的函数
p.sort_stats(SortKey.CUMULATIVE).print_stats(20)
# 想找"某个函数自身太慢":看内部时间(排除被它调用的子函数)
p.sort_stats(SortKey.TIME).print_stats(20)
# 想看哪些函数被调用最频繁:按调用次数
p.sort_stats(SortKey.CALLS).print_stats(20)
# 想定位递归开销:按原始(非递归)调用次数
p.sort_stats(SortKey.PCALLS).print_stats(20)
# 想按代码位置浏览:按函数名(名称排序为升序)
p.sort_stats(SortKey.NAME).print_stats()
当两个函数的首要指标并列时,可追加次键打破平局,例如 sort_stats(SortKey.CUMULATIVE, SortKey.TIME) 让累计耗时相同的函数再按自身耗时细分。想"从另一头看"则可再接 reverse_order()。
合并多份剖析数据:聚合多次运行的统计
性能分析常需跨多次运行聚合,例如对同一基准重复执行若干次以平滑噪声。官方文档给出的两种等价做法:
# 方式一:构造时一次性加载
p = pstats.Stats('run1.prof', 'run2.prof', 'run3.prof')
# 方式二:先建对象,再增量 add
p = pstats.Stats('run1.prof')
p.add('run2.prof')
p.add('run3.prof')
合并时相同函数(文件、行号、函数名一致)的统计会被累加,形成跨多次剖析的聚合视图;各文件的来源路径也会记录在 self.files 中,打印报告时头部会逐一列出。合并后调用者字典按 Lib/pstats.py 的 add_func_stats/add_callers 完成逐位累加,这正是 Lib/test/test_pstats.py AddCallersTestCase 所断言的行为。命令行浏览器同样支持一次打开多个文件(首文件作为初始数据,其余自动 add,见 Lib/pstats.py)。
需要再次提醒:仅当这些文件由同一类剖析器生成时,合并才有意义——混合合并不同剖析器格式的数据会因内部结构不一致而产生误导性结果。
命令行交互式界面:python -m pstats
pstats 可以作为脚本运行,进入一个基于 cmd 模块的行交互式浏览器:
python -m pstats profile_output.prof
启动后提示符为 %,浏览器会打印欢迎语并进入命令循环。键入 help 可随时查看全部命令帮助。核心实现是主程序内的 ProfileBrowser(cmd.Cmd) 类(Lib/pstats.py),并且会尝试导入 readline 以获得行编辑与历史记录支持。
命令速查表(命令名与其调用的 Stats 方法一一对应):
| 命令 | 等价操作 | 说明 |
|---|---|---|
stats |
print_stats |
打印统计报告 |
callers |
print_callers |
打印每个函数的调用者 |
callees |
print_callees |
打印每个函数的被调用者 |
sort |
sort_stats |
按给定键排序;不带参数时列出全部合法键 |
strip |
strip_dirs |
去除文件名中的目录 |
reverse |
reverse_order |
反转排序方向 |
add <file> |
add |
向当前对象追加另一份剖析文件 |
read [file] |
重新加载 | 读取(或重新读取)剖析文件;无参数时重载当前文件 |
quit / EOF |
退出 | 结束浏览器(Ctrl-C 亦可中断) |
stats、callers、callees 三个命令的参数与 Stats 方法中的限制完全一致——整数、[0,1] 内小数、正则字符串三种均可混用,浏览器会在内部按"整数→浮点→字符串"的顺序解析每个词元(见 ProfileBrowser.generic,Lib/pstats.py),并会拒绝超出 [0,1] 的小数限制参数。例如在提示符下输入:
% stats .1 init # 输出前 10% 中名字含 init 的函数
% sort cumulative # 切换到累计时间排序
% stats 20 # 打印前 20 条
% callers .5 # 打印前 50% 条目的调用者
sort 命令支持与库接口一致的唯一前缀缩写,并带 tab 补全(complete_sort 会按已输入文本从全部合法键中筛选,Lib/pstats.py);直接输入 sort(不带参数)则打印全部合法键及其含义。add 遇到加载失败(如文件不存在)会打印 Failed to load statistics for ... 而不会退出浏览器。
结合源码印证:pstats 的完整调用链
把文档描述与源码对应起来,可以完整还原一条"采集 → 落盘 → 分析"链路,便于读者在仓库中继续追溯:
- 数据产生:追踪剖析器执行
run/runctx/Profile并(可选)通过dump_stats或run(..., filename)输出.prof文件;文件内容即一份经marshal序列化、以函数五元组为值的stats字典(Lib/profiling/tracing/init.py 及其中_lsprof底层 C 采集)。需要说明的是,本仓库中历史cProfile模块仍以向后兼容别名形式存在(import cProfile可继续使用,参见 Doc/library/profile.rst),其数据格式与profiling.tracing一致,因此pstats同样可以直接消费cProfile的产出。 - 数据载入:
Stats.__init__→init()→load_stats():对文件走marshal.load,对剖析器对象先调用create_stats()再直接取stats属性(Lib/pstats.py)。 - 分析加工:
sort_stats(构造排序元组 +TupleComp多级比较)→strip_dirs/add/reverse_order等就地变换。 - 渲染输出:
print_stats/print_callers/print_callees通过get_print_list与eval_print_amount统一完成限制过滤,再逐行输出到stream;get_stats_profile则把同一份数据转成结构化的StatsProfile/FunctionProfile对象。 - 持久化:
dump_stats把内存中的stats字典再次marshal落盘,实现分析与采集分离。
对 strip_dirs、add、整数/字符串/枚举三种 sort_stats 参数、限制参数解析、正则过滤、get_stats_profile、SortKey 枚举取值等内容,仓库自带测试 Lib/test/test_pstats.py 均提供了可独立运行的最小验证样例(数据样例取自 Lib/test/pstats.pck,测试通过 support.findfile('pstats.pck') 定位)。深入阅读该文件可快速理解每个 API 的确切契约。
小结
pstats 承担着 Python 剖析体系的"后半程":读取(文件或剖析器对象)、变换(strip/add/sort/reverse)、过滤(整数/百分比/正则)、展示(表格与调用关系视图)与持久化(dump/load)一应俱全。从源码看,它的设计核心是"把每条函数记录抽象为 (cc, nc, tt, ct, callers) 五元组,其余全部操作围绕元组展开"——排序、合并、求调用关系、打印、结构化导出均基于这一模型,因此对追踪式与采样式两种剖析器能够复用同一套管线,仅在量纲上通过 SortKey 与 SampledStats 自动切换。
实用层面的要点浓缩为四条:用 SortKey.CUMULATIVE 找优化热点、用 .1/整数/正则做多级过滤、用 add 聚合多次运行、用 python -m pstats 做无需写代码的交互式下钻。相关配套文档可继续在仓库中阅读 Doc/library/profiling.rst、Doc/library/profiling.tracing.rst、Doc/library/profiling.sampling.rst 以及实现文件 Lib/pstats.py。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00