Python 分析器 — Python 文档

来自菜鸟教程
Python/docs/3.9/library/profile
跳转至:导航、​搜索

Python 分析器

源代码: :source:`Lib/profile.py` and :source:`Lib/pstats.py`



分析器简介

cProfileprofile 提供 Python 程序的 确定性分析profile 是一组统计数据,描述了程序各个部分执行的频率和时间。 这些统计信息可以通过 pstats 模块格式化为报告。

Python 标准库提供了相同分析接口的两种不同实现:

  1. cProfile 推荐给大多数用户; 它是一个具有合理开销的 C 扩展,使其适用于分析长时间运行的程序。 基于 lsprof,由 Brett Rosen 和 Ted Czotter 贡献。
  2. profile,一个纯 Python 模块,它的接口被 cProfile 模仿,但它为分析程序增加了显着的开销。 如果您尝试以某种方式扩展探查器,则使用此模块可能会更轻松地完成任务。 最初由 Jim Roskind 设计和编写。

笔记

分析器模块旨在为给定程序提供执行配置文件,而不是用于基准测试目的(为此,有 timeit 以获得合理准确的结果)。 这尤其适用于针对 C 代码对 Python 代码进行基准测试:分析器引入了 Python 代码的开销,而不是 C 级函数的开销,因此 C 代码看起来比任何 Python 代码都快。


即时用户手册

本节是为“不想阅读手册”的用户提供的。 它提供了一个非常简短的概述,并允许用户快速对现有应用程序进行分析。

要分析采用单个参数的函数,您可以执行以下操作:

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 类从文件中读取配置文件结果并以各种方式对其进行格式化。

文件 cProfileprofile 也可以作为脚本调用来分析另一个脚本。 例如:

python -m cProfile [-o output_file] [-s sort_order] (-m module | myscript.py)

-o 将配置文件结果写入文件而不是标准输出

-s 指定 sort_stats() 排序值之一以对输出进行排序。 这仅适用于未提供 -o 的情况。

-m 指定正在分析模块而不是脚本。

3.7 新功能: cProfile 中添加了 -m 选项。


3.8 新功能:配置文件中增加了-m选项。


pstats 模块的 Stats 类有多种方法来操作和打印保存到配置文件结果文件中的数据:

import pstats
from pstats import SortKey
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()

strip_dirs() 方法从所有模块名称中删除了无关的路径。 sort_stats() 方法根据打印的标准模块/行/名称字符串对所有条目进行排序。 print_stats() 方法打印出所有的统计信息。 您可以尝试以下排序调用:

p.sort_stats(SortKey.NAME)
p.print_stats()

第一次调用实际上将按函数名称对列表进行排序,第二次调用将打印出统计信息。 以下是一些有趣的实验调用:

p.sort_stats(SortKey.CUMULATIVE).print_stats(10)

这按函数中的累积时间对配置文件进行排序,然后只打印十个最重要的行。 如果你想了解什么算法需要时间,上面这行就是你会使用的。

如果您想查看哪些函数循环了很多并且花费了很多时间,您可以这样做:

p.sort_stats(SortKey.TIME).print_stats(10)

根据每个函数花费的时间进行排序,然后打印前十个函数的统计信息。

您也可以尝试:

p.sort_stats(SortKey.FILENAME).print_stats('__init__')

这将按文件名对所有统计信息进行排序,然后仅打印出类 init 方法的统计信息(因为它们在其中拼写为 __init__)。 作为最后一个示例,您可以尝试:

p.sort_stats(SortKey.TIME, SortKey.CUMULATIVE).print_stats(.5, 'init')

这一行用时间的主键和累积时间的辅助键对统计信息进行排序,然后打印出一些统计信息。 具体来说,列表首先被剔除到其原始大小的 50% (re: .5),然后只保留包含 init 的行,并打印该子子列表.

如果您想知道哪些函数调用了上述函数,您现在可以(p 仍然根据最后一个条件排序)执行以下操作:

p.print_callers(.5, 'init')

并且您将获得列出的每个函数的调用者列表。

如果您想要更多功能,您将不得不阅读手册,或者猜测以下功能的作用:

p.print_callees()
p.add('restats')

作为脚本调用,pstats 模块是一个统计浏览器,用于读取和检查配置文件转储。 它有一个简单的面向行的界面(使用 cmd 实现)和交互式帮助。


profile 和 cProfile 模块参考

profilecProfile 模块都提供以下功能:

profile.run(command, filename=None, sort=- 1)

这个函数接受一个可以传递给 exec() 函数的参数和一个可选的文件名。 在所有情况下,此例程都执行:

exec(command, __main__.__dict__, __main__.__dict__)

并从执行中收集分析统计信息。 如果没有文件名,那么这个函数会自动创建一个 Stats 实例并打印一个简单的分析报告。 如果指定了排序值,则将其传递给此 Stats 实例以控制结果的排序方式。

profile.runctx(command, globals, locals, filename=None, sort=- 1)

此函数类似于 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, io
from pstats import SortKey
pr = cProfile.Profile()
pr.enable()
# ... do something ...
pr.disable()
s = io.StringIO()
sortby = SortKey.CUMULATIVE
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
print(s.getvalue())

Profile 类也可以用作上下文管理器(仅在 cProfile 模块中受支持。 参见 上下文管理器类型 ):

import cProfile

with cProfile.Profile() as pr:
    # ... do something ...

pr.print_stats()

3.8 版更改: 添加了上下文管理器支持。

enable()

开始收集分析数据。 仅在 cProfile 中。

disable()

停止收集分析数据。 仅在 cProfile 中。

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)

请注意,仅当被调用的命令/函数实际返回时,分析才会起作用。 如果解释器被终止(例如 在被调用的命令/函数执行期间通过 sys.exit() 调用)不会打印分析结果。


Stats 类

分析器数据的分析是使用 Stats 类完成的。

class pstats.Stats(*filenames or profile, stream=sys.stdout)

此类构造函数从 文件名 (或文件名列表)或 Profile 实例创建“统计对象”的实例。 输出将打印到 stream 指定的流。

上述构造函数选择的文件必须是由profilecProfile对应版本创建的。 具体来说,no 文件兼容性保证与此分析器的未来版本,并且与其他分析器生成的文件或在不同操作系统上运行的相同分析器不兼容。 如果提供多个文件,则将合并相同功能的所有统计信息,以便在单个报告中考虑多个流程的整体视图。 如果其他文件需要与现有 Stats 对象中的数据合并,则可以使用 add() 方法。

cProfile.Profileprofile.Profile 对象可以用作配置文件数据源,而不是从文件中读取配置文件数据。

Stats 对象有以下方法:

strip_dirs()

Stats 类的此方法从文件名中删除所有前导路径信息。 这对于减小打印输出的大小以适应(接近)80 列非常有用。 该方法修改对象,剥离的信息丢失。 执行剥离操作后,该对象被认为具有“随机”顺序的条目,就像在对象初始化和加载之后一样。 如果 strip_dirs() 导致两个函数名无法区分(它们在同一个文件名的同一行,并且具有相同的函数名),则将这两个条目的统计信息累加到一个条目中.

add(*filenames)

Stats 类的这个方法将额外的分析信息累积到当前分析对象中。 它的参数应该引用由 profile.run()cProfile.run() 的相应版本创建的文件名。 同名(re:文件、行、名称)函数的统计信息会自动累积到单个函数统计信息中。

dump_stats(filename)

将加载到 Stats 对象中的数据保存到名为 filename 的文件中。 如果文件不存在,则创建该文件,如果已存在,则覆盖该文件。 这相当于 profile.ProfilecProfile.Profile 类上的同名方法。

sort_stats(*keys)

此方法通过根据提供的标准对其进行排序来修改 Stats 对象。 参数可以是字符串或用于标识排序基础的 SortKey 枚举(例如:'time''name'SortKey.TIMESortKey.NAME)。 SortKey 枚举参数优于字符串参数,因为它更健壮且不易出错。

当提供了多个键时,如果在它们之前选择的所有键都相等,则附加键将用作次要条件。 例如,sort_stats(SortKey.NAME, SortKey.FILE) 将根据函数名对所有条目进行排序,并通过按文件名排序来解析所有关系(相同的函数名)。

对于字符串参数,任何键名都可以使用缩写,只要缩写是明确的。

以下是有效的字符串和 SortKey:

有效字符串参数

有效的枚举参数

意义

'calls'

排序键调用

呼叫计数

'cumulative'

排序键.累积

累计时间

'cumtime'

不适用

累计时间

'file'

不适用

文件名

'filename'

SortKey.FILENAME

文件名

'module'

不适用

文件名

'ncalls'

不适用

呼叫计数

'pcalls'

排序键.PCALLS

原始调用计数

'line'

排序键.LINE

电话号码

'name'

排序键.NAME

函数名

'nfl'

排序键.NFL

名称/文件/行

'stdname'

SortKey.STDNAME

标准名称

'time'

排序键.TIME

内部时间

'tottime'

不适用

内部时间

请注意,统计信息的所有排序均按降序排列(将最耗时的项目放在最前面),其中名称、文件和行号搜索按升序排列(按字母顺序)。 SortKey.NFLSortKey.STDNAME 之间的细微区别在于标准名称是一种打印名称,这意味着嵌入的行号以一种奇怪的方式进行比较。 例如,第 3、20 和 40 行(如果文件名相同)将以字符串顺序 20、3 和 40 出现。 相比之下, SortKey.NFL 对行号进行数字比较。 实际上,sort_stats(SortKey.NFL)sort_stats(SortKey.NAME, SortKey.FILENAME, SortKey.LINE) 是一样的。

出于向后兼容性的原因,允许使用数字参数 -1012。 它们分别被解释为 'stdname''calls''time''cumulative'。 如果使用这种旧样式格式(数字),则只会使用一个排序键(数字键),并且其他参数将被静默忽略。

3.7 新功能: 增加了 SortKey 枚举。

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() 提供的相同,并且限制参数的定义也相同。 每个呼叫者都在自己的线路上报告。 根据生成统计信息的分析器,格式略有不同:

  • 对于 profile,每个呼叫者后面的括号中会显示一个数字,以显示进行了该特定呼叫的次数。 为方便起见,第二个无括号的数字重复在右侧函数中花费的累积时间。

  • 使用 cProfile,每个调用者前面都有三个数字:进行此特定调用的次数,以及在此特定调用者调用当前函数时在当前函数中花费的总时间和累计时间。

print_callees(*restrictions)

Stats 类的此方法打印指定函数调用的所有函数的列表。 除了调用方向的这种反转(重新:被调用与被调用),参数和顺序与 print_callers() 方法相同。

get_stats_profile()

此方法返回 StatsProfile 的实例,其中包含函数名称到 FunctionProfile 实例的映射。 每个 FunctionProfile 实例都包含与函数配置文件相关的信息,例如函数运行的时间、调用的次数等……

3.9 新功能: 添加以下数据类:StatsProfile、FunctionProfile。 添加了以下函数:get_stats_profile。


什么是确定性分析?

确定性分析旨在反映以下事实:所有函数调用函数返回异常事件都受到监控,并且精确计时为这些事件之间的间隔(在这段时间内用户的代码正在执行)。 相比之下,statistical profiling(不是由这个模块完成的)随机采样有效指令指针,并推断出时间花费在哪里。 后一种技术传统上涉及较少的开销(因为不需要检测代码),但仅提供时间花在何处的相对指示。

在 Python 中,由于在执行期间有一个解释器处于活动状态,因此不需要存在检测代码来进行确定性分析。 Python 自动为每个事件提供一个 hook(可选回调)。 此外,Python 的解释性质往往会给执行增加如此多的开销,以至于确定性分析往往只会在典型应用程序中增加很小的处理开销。 结果是确定性分析并不那么昂贵,但提供了有关 Python 程序执行的大量运行时统计信息。

调用计数统计可用于识别代码中的错误(令人惊讶的计数),并识别可能的内联扩展点(高调用计数)。 内部时间统计可用于识别应仔细优化的“热循环”。 应使用累积时间统计来识别算法选择中的高级错误。 请注意,此分析器中对累积时间的异常处理允许将算法的递归实现的统计信息直接与迭代实现进行比较。


限制

一个限制与计时信息的准确性有关。 确定性分析器存在一个涉及准确性的基本问题。 最明显的限制是底层“时钟”仅以大约 0.001 秒的速率(通常)滴答作响。 因此,没有任何测量会比基础时钟更准确。 如果进行了足够的测量,那么“误差”将趋于平均。 不幸的是,消除第一个错误会导致第二个错误源。

第二个问题是从调度事件到分析器调用获取时间实际上 获取 时钟状态“需要一段时间”。 类似地,从获取时钟值(然后撤回)到用户代码再次执行,退出分析器事件处理程序也有一定的延迟。 因此,多次调用或调用许多函数的函数通常会累积此错误。 以这种方式累积的误差通常小于时钟的精度(小于一个时钟滴答),但它 可以 累积并变得非常重要。

profile 的问题比开销较低的 cProfile 更重要。 出于这个原因,profile 提供了一种针对给定平台校准自身的方法,以便可以概率地(平均地)消除此错误。 分析器校准后,它会更准确(在最小二乘意义上),但有时会产生负数(当调用计数异常低时,概率之神对你不利:-)。 ) 不要不要被配置文件中的负数吓到。 如果您已经校准了分析器,它们应该 only 出现,并且结果实际上比没有校准要好。


校准

profile 模块的分析器从每个事件处理时间中减去一个常量,以补偿调用 time 函数和存储结果的开销。 默认情况下,常量为 0。 以下过程可用于为给定平台获得更好的常数(请参阅 限制 )。

import profile
pr = profile.Profile()
for i in range(5):
    print(pr.calibrate(10000))

该方法直接在探查器下执行参数给出的 Python 调用次数,并测量两者的时间。 然后计算每个分析器事件的隐藏开销,并将其作为浮点数返回。 例如,在运行 macOS 的 1.8Ghz Intel Core i5 上,使用 Python 的 time.process_time() 作为计时器,神奇数字约为 4.04e-6。

这个练习的目的是获得一个相当一致的结果。 如果您的计算机 非常 快,或者您的计时器功能分辨率不佳,您可能需要通过 100000,甚至 1000000,才能获得一致的结果。

当你有一个一致的答案时,你可以通过三种方式使用它:

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)

如果您有选择,您最好选择一个较小的常数,这样您的结果将“较少”在配置文件统计中显示为负数。


使用自定义计时器

如果要更改确定当前时间的方式(例如,强制使用挂钟时间或经过的处理时间),请将所需的计时函数传递给 Profile 类构造函数:

pr = profile.Profile(your_time_func)

然后生成的分析器将调用 your_time_func。 根据您使用的是 profile.Profile 还是 cProfile.Profileyour_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 源代码中对其进行硬编码。

Python 3.3 在 time 中添加了几个新函数,可用于精确测量进程或挂钟时间。 例如,参见 time.perf_counter()