首页
/ 使用 pstats 分析 Python 剖析统计:Stats、排序键、过滤器与交互式命令行完全指南

使用 pstats 分析 Python 剖析统计:Stats、排序键、过滤器与交互式命令行完全指南

2026-09-07 17:53:34作者:卓炯娓

导读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 负责后续所有"读、算、排、筛、印"环节。这一点可以从本仓库的结构看出:

官方文档在"参见(see also)"一节中把 pstats 定义为三条配套文档的公共下游:profiling 提供整体概览,profiling.tracing 是确定性追踪剖析器,profiling.sampling 是统计采样剖析器。也就是说,无论你用哪一端产出数据文件,都可以用同一个 pstats 工具链完成分析。

需要特别说明的是:剖析数据文件格式只对生成它的 Python 版本有效。官方文档明确写道,不同 Python 版本之间、不同剖析器之间不存在格式兼容性保证。原因可以从实现找到——Stats.load_statsdump_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.pyload_stats 中可以看到一个重要的自动分派逻辑:如果文件内容里含 ('__sampled__',) 标记,说明这是统计采样剖析器生成的数据,pstats 会弹出该标记并把对象类切换为 SampledStats(采样统计专用子类),从而让表头、排序键和含义整体切换为"样本(sample)"语义。这印证了文档所述"pstats 同时支持两类剖析器输出"。

strip_dirs():压缩文件名

strip_dirs() 把所有文件名中前导目录信息去掉(保留 basename)。它是就地修改并返回 self 以便链式调用。源码 Lib/pstats.py 展示了两个值得注意的副作用:

  1. 目录被剥离后,函数之间的调用者/被调用者关系会一并重算(调用者的文件名同样被 strip,func_strip_path 使用 os.path.basename)。
  2. 若不同目录下出现"同名文件、同行号、同函数名"的两个条目,它们会通过 add_func_stats合并为同一条统计。
  3. 文档特别提示:执行完 strip_dirs() 后,若尚未 sort_stats,统计数据处于"随机顺序"状态——源码中 strip_dirs() 会把 fcn_listNone,而打印时的排序列表正是由 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.pysort_arg_dict_default 中每个键的方向标记(-1 降序 / 1 升序)决定,最终由 TupleComp.compareLib/pstats.py)执行多级比较。

关于 NFLSTDNAME 的差异:两者都按"名称→文件→行号"排序,但 NFL行号当作数值比较,而 STDNAME 按整个标准名字符串 "文件:行号(函数名)" 做字符串比较。此外,sort_stats(SortKey.NFL)sort_stats(SortKey.NAME, SortKey.FILENAME, SortKey.LINE) 完全等价。

兼容旧版 profile 的数值参数:为保证向后兼容,-1012 四个整数仍被接受,分别对应 '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)可以看到报告头部由以下几行组成:

  1. 数据来源文件名(含文件修改时间)列表;
  2. 被判定为"顶层入口"的函数名列表;
  3. 汇总行:总函数调用数 in 总耗时秒,当总调用数与原始(非递归)调用数不等时还会额外标注 (N primitive calls)
  4. 随后是按最后一次 sort_stats 排序的函数统计表。

表体标题行固定为(见 print_titleLib/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_lineLib/pstats.py)。该五元组结构在追踪剖析器端由 Lib/profiling/tracing/init.pysnapshot_stats 组装,cc = nc - reccallcount 正是对递归调用的剔除。

限制(restriction)的三种类型(由 Lib/pstats.pyeval_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 cumtimeLib/pstats.py)。

从实现角度,调用者关系直接取自每条记录的 callers 字典;print_callees 需要先把所有记录的 callers 关系"反查"并缓存为 all_callees,这一倒排计算由 calc_calleesLib/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,字段为 ncallstottimepercall_tottimecumtimepercall_cumtimefile_nameline_numberLib/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.pyadd_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 亦可中断)

statscallerscallees 三个命令的参数与 Stats 方法中的限制完全一致——整数、[0,1] 内小数、正则字符串三种均可混用,浏览器会在内部按"整数→浮点→字符串"的顺序解析每个词元(见 ProfileBrowser.genericLib/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 的完整调用链

把文档描述与源码对应起来,可以完整还原一条"采集 → 落盘 → 分析"链路,便于读者在仓库中继续追溯:

  1. 数据产生:追踪剖析器执行 run/runctx/Profile 并(可选)通过 dump_statsrun(..., filename) 输出 .prof 文件;文件内容即一份经 marshal 序列化、以函数五元组为值的 stats 字典(Lib/profiling/tracing/init.py 及其中 _lsprof 底层 C 采集)。需要说明的是,本仓库中历史 cProfile 模块仍以向后兼容别名形式存在(import cProfile 可继续使用,参见 Doc/library/profile.rst),其数据格式与 profiling.tracing 一致,因此 pstats 同样可以直接消费 cProfile 的产出。
  2. 数据载入Stats.__init__init()load_stats():对文件走 marshal.load,对剖析器对象先调用 create_stats() 再直接取 stats 属性(Lib/pstats.py)。
  3. 分析加工sort_stats(构造排序元组 + TupleComp 多级比较)→ strip_dirs/add/reverse_order 等就地变换。
  4. 渲染输出print_stats/print_callers/print_callees 通过 get_print_listeval_print_amount 统一完成限制过滤,再逐行输出到 streamget_stats_profile 则把同一份数据转成结构化的 StatsProfile/FunctionProfile 对象。
  5. 持久化dump_stats 把内存中的 stats 字典再次 marshal 落盘,实现分析与采集分离。

strip_dirsadd、整数/字符串/枚举三种 sort_stats 参数、限制参数解析、正则过滤、get_stats_profileSortKey 枚举取值等内容,仓库自带测试 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) 五元组,其余全部操作围绕元组展开"——排序、合并、求调用关系、打印、结构化导出均基于这一模型,因此对追踪式与采样式两种剖析器能够复用同一套管线,仅在量纲上通过 SortKeySampledStats 自动切换。

实用层面的要点浓缩为四条:用 SortKey.CUMULATIVE 找优化热点、用 .1/整数/正则做多级过滤、用 add 聚合多次运行、用 python -m pstats 做无需写代码的交互式下钻。相关配套文档可继续在仓库中阅读 Doc/library/profiling.rstDoc/library/profiling.tracing.rstDoc/library/profiling.sampling.rst 以及实现文件 Lib/pstats.py

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391