26.4. Python 分析器 — Python 文档
26.4. Python 分析器
源代码: :source:`Lib/profile.py` and :source:`Lib/pstats.py`
26.4.1. 分析器简介
cProfile 和 profile 提供 Python 程序的 确定性分析 。 profile 是一组统计数据,描述了程序各个部分执行的频率和时间。 这些统计信息可以通过 pstats 模块格式化为报告。
Python 标准库提供了相同分析接口的三种不同实现:
cProfile 推荐给大多数用户; 它是一个具有合理开销的 C 扩展,使其适用于分析长时间运行的程序。 基于
lsprof
,由 Brett Rosen 和 Ted Czotter 贡献。2.5 版中的新功能。
profile,一个纯 Python 模块,它的接口被 cProfile 模仿,但它为分析程序增加了显着的开销。 如果您尝试以某种方式扩展探查器,则使用此模块可能会更轻松地完成任务。 最初由 Jim Roskind 设计和编写。
2.4 版更改: 现在还报告调用内置函数和方法所花费的时间。
hotshot 是一个实验性的 C 模块,专注于最小化分析的开销,代价是更长的数据后处理时间。 它不再维护,可能会在 Python 的未来版本中删除。
2.5 版本变更: 结果应该比过去更有意义:计时核心包含一个严重错误。
profile和cProfile模块导出相同的接口,所以它们大多可以互换; cProfile 的开销要低得多,但较新,可能并非在所有系统上都可用。 cProfile 实际上是内部 _lsprof
模块之上的兼容层。 hotshot 模块保留用于特殊用途。
笔记
分析器模块旨在为给定程序提供执行配置文件,而不是用于基准测试目的(为此,有 timeit 以获得合理准确的结果)。 这尤其适用于针对 C 代码对 Python 代码进行基准测试:分析器引入了 Python 代码的开销,而不是 C 级函数的开销,因此 C 代码看起来比任何 Python 代码都快。
26.4.2. 即时用户手册
本节是为“不想阅读手册”的用户提供的。 它提供了一个非常简短的概述,并允许用户快速对现有应用程序进行分析。
要分析采用单个参数的函数,您可以执行以下操作:
import cProfile
import re
cProfile.run('re.compile("foo|bar")')
(如果后者在您的系统上不可用,请使用 profile 而不是 cProfile。)
上述操作将运行 re.compile() 并打印如下所示的配置文件结果:
197 function calls (192 primitive calls) in 0.002 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.001 0.001 <string>:1(<module>)
1 0.000 0.000 0.001 0.001 re.py:212(compile)
1 0.000 0.000 0.001 0.001 re.py:268(_compile)
1 0.000 0.000 0.000 0.000 sre_compile.py:172(_compile_charset)
1 0.000 0.000 0.000 0.000 sre_compile.py:201(_optimize_charset)
4 0.000 0.000 0.000 0.000 sre_compile.py:25(_identityfunction)
3/1 0.000 0.000 0.000 0.000 sre_compile.py:33(_compile)
第一行表示监控了 197 个呼叫。 在这些调用中,192 个是 primitive,这意味着调用不是通过递归引起的。 下一行:Ordered by: standard name
,表示使用最右列中的文本字符串对输出进行排序。 列标题包括:
- 调用
- 对于通话次数,
- 时间
- 在给定函数中花费的总时间(不包括调用子函数的时间)
- 每次通话
- 是
tottime
除以ncalls
的商 - 临时工
- 是在这个函数和所有子函数中花费的累积时间(从调用到退出)。 对于递归函数,这个数字是准确的 甚至 。
- 每次通话
- 是
cumtime
除以原始调用的商 - 文件名:lineno(函数)
- 提供每个函数的各自数据
当第一列有两个数字时(例如3/1
),表示函数递归了。 第二个值是原始调用的数量,前者是调用的总数。 注意函数不递归时,这两个值是一样的,只打印单个数字。
您可以通过为 run()
函数指定文件名,而不是在配置文件运行结束时打印输出:
import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')
pstats.Stats 类从文件中读取配置文件结果并以各种方式对其进行格式化。
文件 cProfile 也可以作为脚本调用来分析另一个脚本。 例如:
python -m cProfile [-o output_file] [-s sort_order] myscript.py
-o
将配置文件结果写入文件而不是标准输出
-s
指定 sort_stats() 排序值之一以对输出进行排序。 这仅适用于未提供 -o
的情况。
pstats 模块的 Stats 类有多种方法来操作和打印保存到配置文件结果文件中的数据:
import pstats
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()
strip_dirs() 方法从所有模块名称中删除了无关的路径。 sort_stats() 方法根据打印的标准模块/行/名称字符串对所有条目进行排序。 print_stats() 方法打印出所有的统计信息。 您可以尝试以下排序调用:
p.sort_stats('name')
p.print_stats()
第一次调用实际上将按函数名称对列表进行排序,第二次调用将打印出统计信息。 以下是一些有趣的实验调用:
p.sort_stats('cumulative').print_stats(10)
这按函数中的累积时间对配置文件进行排序,然后只打印十个最重要的行。 如果你想了解什么算法需要时间,上面这行就是你会使用的。
如果您想查看哪些函数循环了很多并且花费了很多时间,您可以这样做:
p.sort_stats('time').print_stats(10)
根据每个函数花费的时间进行排序,然后打印前十个函数的统计信息。
您也可以尝试:
p.sort_stats('file').print_stats('__init__')
这将按文件名对所有统计信息进行排序,然后仅打印出类 init 方法的统计信息(因为它们在其中拼写为 __init__
)。 作为最后一个示例,您可以尝试:
p.sort_stats('time', 'cum').print_stats(.5, 'init')
这一行用时间的主键和累积时间的辅助键对统计信息进行排序,然后打印出一些统计信息。 具体来说,列表首先被剔除到其原始大小的 50% (re: .5
),然后只保留包含 init
的行,并打印该子子列表.
如果您想知道哪些函数调用了上述函数,您现在可以(p
仍然根据最后一个条件排序)执行以下操作:
p.print_callers(.5, 'init')
并且您将获得列出的每个函数的调用者列表。
如果您想要更多功能,您将不得不阅读手册,或者猜测以下功能的作用:
p.print_callees()
p.add('restats')
作为脚本调用,pstats 模块是一个统计浏览器,用于读取和检查配置文件转储。 它有一个简单的面向行的界面(使用 cmd 实现)和交互式帮助。
26.4.3. 轮廓和个人资料模块参考
- profile.run(command, filename=None, sort=- 1)
这个函数接受一个可以传递给
exec()
函数的参数和一个可选的文件名。 在所有情况下,此例程都执行:exec(command, __main__.__dict__, __main__.__dict__)
并从执行中收集分析统计信息。 如果没有文件名,那么这个函数会自动创建一个 Stats 实例并打印一个简单的分析报告。 如果指定了排序值,则将其传递给此 Stats 实例以控制结果的排序方式。
- profile.runctx(command, globals, locals, filename=None)
此函数类似于 run(),添加了参数以提供 command 字符串的全局和局部字典。 该例程执行:
exec(command, globals, locals)
并像上面的 run() 函数一样收集分析统计信息。
- class profile.Profile(timer=None, timeunit=0.0, subcalls=True, builtins=True)
此类通常仅在需要比
cProfile.run()
函数提供的更精确的分析控制时才使用。可以提供自定义计时器,用于通过 timer 参数测量代码运行所需的时间。 这必须是一个返回表示当前时间的单个数字的函数。 如果数字是整数,则 timeunit 指定一个乘数,用于指定每个时间单位的持续时间。 例如,如果计时器返回以千秒为单位测量的时间,则时间单位将为
.001
。直接使用 Profile 类允许在不将配置文件数据写入文件的情况下格式化配置文件结果:
import cProfile, pstats, StringIO pr = cProfile.Profile() pr.enable() # ... do something ... pr.disable() s = StringIO.StringIO() sortby = 'cumulative' ps = pstats.Stats(pr, stream=s).sort_stats(sortby) ps.print_stats() print s.getvalue()
- enable()
开始收集分析数据。
- disable()
停止收集分析数据。
- create_stats()
停止收集分析数据并在内部将结果记录为当前分析。
- print_stats(sort=- 1)
根据当前配置文件创建一个 Stats 对象并将结果打印到标准输出。
- dump_stats(filename)
将当前配置文件的结果写入 filename。
- run(cmd)
通过
exec()
配置 cmd。
- runctx(cmd, globals, locals)
通过
exec()
使用指定的全局和本地环境分析 cmd。
- runcall(func, *args, **kwargs)
型材
func(*args, **kwargs)
26.4.4. 这Stats班级
分析器数据的分析是使用 Stats 类完成的。
- class pstats.Stats(*filenames or profile, stream=sys.stdout)
此类构造函数从 文件名 (或文件名列表)或
Profile
实例创建“统计对象”的实例。 输出将打印到 stream 指定的流。上述构造函数选择的文件必须是由profile或cProfile对应版本创建的。 具体来说,no 文件兼容性保证与此分析器的未来版本,并且与其他分析器生成的文件或在不同操作系统上运行的相同分析器不兼容。 如果提供多个文件,则将合并相同功能的所有统计信息,以便在单个报告中考虑多个流程的整体视图。 如果其他文件需要与现有 Stats 对象中的数据合并,则可以使用 add() 方法。
cProfile.Profile
或 profile.Profile 对象可以用作配置文件数据源,而不是从文件中读取配置文件数据。Stats 对象有以下方法:
- strip_dirs()
Stats 类的此方法从文件名中删除所有前导路径信息。 这对于减小打印输出的大小以适应(接近)80 列非常有用。 该方法修改对象,剥离的信息丢失。 执行剥离操作后,该对象被认为具有“随机”顺序的条目,就像在对象初始化和加载之后一样。 如果 strip_dirs() 导致两个函数名无法区分(它们在同一个文件名的同一行,并且具有相同的函数名),则将这两个条目的统计信息累加到一个条目中.
- add(*filenames)
Stats 类的这个方法将额外的分析信息累积到当前分析对象中。 它的参数应该引用由 profile.run() 或
cProfile.run()
的相应版本创建的文件名。 同名(re:文件、行、名称)函数的统计信息会自动累积到单个函数统计信息中。
- dump_stats(filename)
将加载到 Stats 对象中的数据保存到名为 filename 的文件中。 如果文件不存在,则创建该文件,如果已存在,则覆盖该文件。 这相当于 profile.Profile 和
cProfile.Profile
类上的同名方法。
2.3 版中的新功能。
- sort_stats(*keys)
此方法通过根据提供的标准对其进行排序来修改 Stats 对象。 参数通常是标识排序基础的字符串(例如:
'time'
或'name'
)。当提供了多个键时,如果在它们之前选择的所有键都相等,则附加键将用作次要条件。 例如,
sort_stats('name', 'file')
将根据函数名对所有条目进行排序,并通过按文件名排序来解析所有关系(相同的函数名)。缩写可用于任何键名,只要缩写是明确的。 以下是当前定义的键:
有效参数
意义
'calls'
呼叫计数
'cumulative'
累计时间
'cumtime'
累计时间
'file'
文件名
'filename'
文件名
'module'
文件名
'ncalls'
呼叫计数
'pcalls'
原始调用计数
'line'
电话号码
'name'
函数名
'nfl'
名称/文件/行
'stdname'
标准名称
'time'
内部时间
'tottime'
内部时间
请注意,统计信息的所有排序均按降序排列(将最耗时的项目放在最前面),其中名称、文件和行号搜索按升序排列(按字母顺序)。
'nfl'
和'stdname'
之间的细微区别在于标准名称是一种打印名称,这意味着嵌入的行号以一种奇怪的方式进行比较。 例如,第 3、20 和 40 行(如果文件名相同)将以字符串顺序 20、3 和 40 出现。 相比之下,'nfl'
对行号进行数字比较。 实际上,sort_stats('nfl')
和sort_stats('name', 'file', 'line')
是一样的。出于向后兼容性的原因,允许使用数字参数
-1
、0
、1
和2
。 它们分别被解释为'stdname'
、'calls'
、'time'
和'cumulative'
。 如果使用这种旧样式格式(数字),则只会使用一个排序键(数字键),并且其他参数将被静默忽略。
- reverse_order()
Stats 类的此方法颠倒了对象内基本列表的顺序。 请注意,默认情况下,根据选择的排序键正确选择升序与降序。
- print_stats(*restrictions)
Stats 类的这个方法打印出一个报告,如 profile.run() 定义中所述。
打印顺序基于对对象执行的最后一次 sort_stats() 操作(受 add() 和 strip_dirs() 中的警告影响)。
提供的参数(如果有)可用于将列表限制为重要条目。 最初,该列表被视为完整的分析函数集。 每个限制要么是一个整数(选择行数),要么是介于 0.0 和 1.0 之间的小数(包括行数)或正则表达式(模式匹配打印的标准名称)。 如果提供了多个限制,则它们将按顺序应用。 例如:
print_stats(.1, 'foo:')
将首先限制打印到第一个 10% of 列表,然后只打印属于文件名
.*foo:
的函数。 相比之下,命令:print_stats('foo:', .1)
会将列表限制为具有文件名
.*foo:
的所有函数,然后继续仅打印前 10% of 个函数。
- print_callers(*restrictions)
Stats 类的这个方法打印了所有函数的列表,这些函数调用了分析数据库中的每个函数。 顺序与 print_stats() 提供的相同,并且限制参数的定义也相同。 每个呼叫者都在自己的线路上报告。 根据生成统计信息的分析器,格式略有不同:
- print_callees(*restrictions)
Stats 类的此方法打印指定函数调用的所有函数的列表。 除了调用方向的这种反转(重新:被调用与被调用),参数和顺序与 print_callers() 方法相同。
26.4.5. 什么是确定性分析?
确定性分析旨在反映以下事实:所有函数调用、函数返回和异常事件都受到监控,并且精确计时为这些事件之间的间隔(在这段时间内用户的代码正在执行)。 相比之下,statistical profiling(不是由这个模块完成的)随机采样有效指令指针,并推断出时间花费在哪里。 后一种技术传统上涉及较少的开销(因为不需要检测代码),但仅提供时间花在何处的相对指示。
在 Python 中,由于在执行期间有一个解释器处于活动状态,因此进行确定性分析不需要存在检测代码。 Python 自动为每个事件提供一个 hook(可选回调)。 此外,Python 的解释性质往往会给执行增加如此多的开销,以至于确定性分析往往只会在典型应用程序中增加很小的处理开销。 结果是确定性分析并不那么昂贵,但提供了有关 Python 程序执行的大量运行时统计信息。
调用计数统计可用于识别代码中的错误(令人惊讶的计数),并识别可能的内联扩展点(高调用计数)。 内部时间统计可用于识别应仔细优化的“热循环”。 应使用累积时间统计来识别算法选择中的高级错误。 请注意,此分析器中对累积时间的异常处理允许将算法的递归实现的统计信息直接与迭代实现进行比较。
26.4.6. 限制
一个限制与计时信息的准确性有关。 确定性分析器存在一个涉及准确性的基本问题。 最明显的限制是底层“时钟”仅以大约 0.001 秒的速率(通常)滴答作响。 因此,没有任何测量会比基础时钟更准确。 如果进行了足够的测量,那么“误差”将趋于平均。 不幸的是,消除第一个错误会导致第二个错误源。
第二个问题是从调度事件到分析器调用获取时间实际上 获取 时钟状态“需要一段时间”。 类似地,从获取时钟值(然后撤回)到用户代码再次执行,退出分析器事件处理程序也有一定的延迟。 因此,多次调用或调用许多函数的函数通常会累积此错误。 以这种方式累积的误差通常小于时钟的精度(小于一个时钟滴答),但它 可以 累积并变得非常重要。
profile 的问题比开销较低的 cProfile 更重要。 出于这个原因,profile 提供了一种针对给定平台校准自身的方法,以便可以概率地(平均地)消除此错误。 分析器校准后,它会更准确(在最小二乘意义上),但有时会产生负数(当调用计数异常低时,概率之神对你不利:-)。 ) 不要不要被配置文件中的负数吓到。 如果您已经校准了分析器,它们应该 only 出现,并且结果实际上比没有校准要好。
26.4.7. 校准
profile 模块的分析器从每个事件处理时间中减去一个常量,以补偿调用 time 函数和存储结果的开销。 默认情况下,常量为 0。 以下过程可用于为给定平台获得更好的常数(请参阅 限制 )。
import profile
pr = profile.Profile()
for i in range(5):
print pr.calibrate(10000)
该方法直接在探查器下执行参数给出的 Python 调用次数,并测量两者的时间。 然后计算每个分析器事件的隐藏开销,并将其作为浮点数返回。 例如,在运行 Mac OS X 的 1.8Ghz Intel Core i5 上,使用 Python 的 time.clock() 作为计时器,神奇的数字约为 4.04e-6。
这个练习的目的是获得一个相当一致的结果。 如果您的计算机 非常 快,或者您的计时器功能分辨率不佳,您可能需要通过 100000,甚至 1000000,才能获得一致的结果。
当你有一个一致的答案时,你可以通过三种方式使用它:1
import profile
# 1. Apply computed bias to all Profile instances created hereafter.
profile.Profile.bias = your_computed_bias
# 2. Apply computed bias to a specific Profile instance.
pr = profile.Profile()
pr.bias = your_computed_bias
# 3. Specify computed bias in instance constructor.
pr = profile.Profile(bias=your_computed_bias)
如果您有选择,您最好选择一个较小的常数,这样您的结果将“较少”在配置文件统计中显示为负数。
26.4.8. 使用自定义计时器
如果要更改确定当前时间的方式(例如,强制使用挂钟时间或经过的处理时间),请将所需的计时函数传递给 Profile
类构造函数:
pr = profile.Profile(your_time_func)
然后生成的分析器将调用 your_time_func
。 根据您使用的是 profile.Profile 还是 cProfile.Profile
,your_time_func
的返回值将有不同的解释:
profile.Profile
your_time_func
应该返回一个数字,或者一个数字列表,其总和是当前时间(就像 os.times() 返回的)。 如果函数返回单个时间数字,或者返回数字列表的长度为 2,那么您将获得调度例程的特别快速版本。请注意,您应该为您选择的计时器函数校准分析器类(请参阅 Calibration)。 对于大多数机器,返回一个单独的整数值的计时器将在分析期间提供低开销方面的最佳结果。 (os.times() 是 pretty 不好,因为它返回一个浮点值元组)。 如果您想以最简洁的方式替换更好的计时器,请派生一个类并硬连接一个最能处理您的计时器调用的替换调度方法,以及适当的校准常量。
cProfile.Profile
your_time_func
应该返回一个数字。 如果它返回整数,您还可以使用指定一个单位时间的实际持续时间的第二个参数调用类构造函数。 例如,如果your_integer_time_func
返回以千秒为单位测量的时间,您将按如下方式构造Profile
实例:pr = cProfile.Profile(your_integer_time_func, 0.001)
由于
cProfile.Profile
类无法校准,因此应谨慎使用自定义定时器功能,并应尽可能快。 为了使用自定义计时器获得最佳结果,可能需要在内部_lsprof
模块的 C 源代码中对其进行硬编码。
脚注
- 1
- 在 Python 2.2 之前,有必要编辑分析器源代码以将偏差嵌入为文字数字。 您仍然可以,但不再描述该方法,因为不再需要。