Python 性能分析器入门教程

2025-07-02 12:03 更新

Python 编程过程中,性能分析是一个至关重要的环节。性能分析可以帮助我们找出程序中的瓶颈,从而优化代码,提高程序的运行效率。Python 标准库提供了 cProfileprofile 模块来实现确定性性能分析。本文将深入浅出地为您介绍这两个模块的使用方法,让您轻松掌握 Python 性能分析技巧。

确定性性能分析 是指监控程序中所有的函数调用、函数返回和异常事件,并精确计时这些事件之间的时间间隔。与统计分析相比,确定性分析能够提供更详细、准确的运行时统计信息,帮助我们更好地了解程序的性能状况。

一、cProfileprofile 模块简介

Python 提供了两种性能分析模块:

  1. cProfile :这是一个 C 扩展插件,具有较低的运行开销,适合分析长时间运行的程序。它基于 lsprof 开发,是大多数用户的首选。
  2. profile :这是一个纯 Python 模块,虽然运行开销较大,但更易于扩展和定制。如果需要对分析器进行深入的二次开发,这个模块会更加合适。

通常情况下,我们推荐使用 cProfile 模块,因为它在性能分析过程中对程序运行的影响较小。

二、性能分析基础操作

1. 分析单个函数

以下是使用 cProfile 模块分析单个函数的简单示例:

import cProfile
import re
cProfile.run('re.compile("foo|bar")')

执行上述代码后,将输出类似于以下的分析结果:

214 function calls (207 primitive calls) in 0.002 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
    1 0.000 0.000 0.002 0.002 {built-in method builtins.exec}
    1 0.000 0.000 0.001 0.001 <string>:1(<module>)
    1 0.000 0.000 0.001 0.001 __init__.py:250(compile)
    1 0.000 0.000 0.001 0.001 __init__.py:289(_compile)
    1 0.000 0.000 0.000 0.000 _compiler.py:759(compile)
    1 0.000 0.000 0.000 0.000 _parser.py:937(parse)
    1 0.000 0.000 0.000 0.000 _compiler.py:598(_code)
    1 0.000 0.000 0.000 0.000 _parser.py:435(_parse_sub)

结果解读

  • 第一行显示共有 214 次函数调用,其中 207 次为原始调用(非递归调用)。

  • 第二行表示输出结果是按照累计时间(cumulative time)排序的。

  • 各列含义如下:

    • ncalls :函数调用次数。
    • tottime :在该函数中执行所花费的总时间(不包括调用子函数的时间)。
    • percalltottimencalls 的比值,表示每次调用该函数的平均时间。
    • cumtime :该函数及其所有子函数的累计执行时间。
    • percallcumtime 与原始调用次数的比值。
    • filename:lineno(function) :函数所在的文件名、行号和函数名。

2. 保存分析结果

如果不想直接打印分析结果,可以将其保存到文件中,以便后续分析:

import cProfile
import re
cProfile.run('re.compile("foo|bar")', 'restats')

此时,分析结果会被保存到名为 restats 的文件中。之后可以使用 pstats.Stats 类来读取和处理这些结果。

3. 命令行分析

cProfileprofile 模块还可以作为脚本直接调用,用于分析其他脚本的性能。例如:

python -m cProfile [-o output_file] [-s sort_order] (-m module | myscript.py)
  • -o output_file :将性能分析结果保存到指定文件中,而不是输出到标准输出。
  • -s sort_order :指定排序方式,例如按时间排序(time)、按累计时间排序(cumulative)等。
  • -m module :指定要分析的模块而不是脚本。

三、pstats.Stats 类的使用

pstats.Stats 类提供了丰富的功能,用于分析和格式化性能分析结果。

1. 基本读取和打印

import pstats
from pstats import SortKey
p = pstats.Stats('restats')
p.strip_dirs().sort_stats(-1).print_stats()
  • strip_dirs() :从函数文件名中移除多余的路径信息,使输出更简洁。
  • sort_stats(-1) :对分析结果进行排序,-1 表示按标准名称排序。
  • print_stats() :打印分析结果。

2. 按不同条件排序

可以按不同的条件对分析结果进行排序,以便更直观地了解程序性能。

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

此代码按函数名称排序并打印结果。

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

此处按累计时间排序,并打印前 10 行结果,有助于快速定位程序中的性能瓶颈。

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

按每个函数自身的执行时间排序,同样显示前 10 行。

3. 筛选和查找

可以根据特定条件筛选分析结果,例如:

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

按文件名排序,并打印所有包含 __init__ 的函数的分析结果。

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

先按时间排序,再按累计时间排序,打印出原始结果 50% 的数据中包含 init 的相关函数信息。

还可以查看函数的调用者和被调用者:

p.print_callers(.5, 'init')

显示调用了包含 init 的函数的调用者列表。

p.print_callees()

打印被指定函数调用的所有函数列表。

四、profile.Profile 类的使用

在需要更精确地控制性能分析过程时,可以直接使用 profile.Profile 类。

1. 基本性能分析

import cProfile, pstats, io
from pstats import SortKey
pr = cProfile.Profile()
pr.enable()
# 在这里执行要分析的代码
pr.disable()
s = io.StringIO()
sortby = SortKey.CUMULATIVE
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
ps.print_stats()
print(s.getvalue())
  • enable() :开始收集性能分析数据。
  • disable() :停止收集数据。

也可以使用上下文管理器来简化代码:

import cProfile

with cProfile.Profile() as pr:
    # 在这里执行代码
    pr.print_stats()

2. 其他方法

profile.Profile 类还提供了以下方法:

  • create_stats() :停止收集数据,并将结果记录为当前 profile。
  • print_stats(sort=-1) :根据当前性能分析数据创建一个 Stats 对象并打印结果,sort 参数用于指定排序方式。
  • dump_stats(filename) :将性能分析结果写入指定文件。
  • run(cmd) :对指定命令进行性能分析。
  • runctx(cmd, globals, locals) :在指定的全局和局部环境下对命令进行性能分析。
  • runcall(func, /, *args, **kwargs) :对函数调用进行性能分析。

五、性能分析结果解读与应用

通过性能分析得到的数据,我们可以深入理解程序的运行情况,从而进行有针对性的优化。

1. 识别性能瓶颈

重点关注 tottimecumtime 较高的函数,这些函数可能是程序的性能瓶颈。例如,如果某个函数的 tottime 很高,说明该函数自身的执行效率较低,可能需要优化其算法或实现方式;如果 cumtime 较高而 tottime 相对较低,则表明该函数调用了许多其他耗时函数,可能需要对整体的函数调用结构进行优化。

2. 优化函数调用

根据 ncalls 列,找出调用次数过多的函数。如果一个函数被频繁调用,但其实可以合并或减少调用次数,那么优化这部分代码将对程序性能产生显著提升。例如,将一些重复的计算移到循环外部,或者使用更高效的数据结构来减少不必要的函数调用。

3. 分析递归函数

对于递归函数,性能分析结果中的 ncalls 列可能会显示多个调用次数(例如显示为 3/1),其中第一个数字是总调用次数,第二个数字是原始调用次数。通过这种方式,我们可以了解递归函数的调用深度和频率,从而判断是否需要对递归实现进行优化,或者考虑使用迭代代替递归以提高性能。

4. 持续监测与迭代优化

性能分析不是一劳永逸的工作,而是一个持续的过程。在对程序进行优化后,再次进行性能分析,对比优化前后的数据,评估优化效果。根据新的分析结果,继续寻找潜在的性能瓶颈并进行优化,通过不断的迭代,逐步提升程序的性能表现。

六、自定义计时器与准确估量

1. 自定义计时器

如果需要改变时间测量方式,可以向 Profile 类构造器传入自定义的计时函数:

import time
pr = profile.Profile(time.perf_counter)
  • 对于 profile.Profile ,计时函数可以返回一个数字或数字列表。
  • 对于 cProfile.Profile ,计时函数应返回一个数字,如果返回整数,还可以通过第二个参数指定单位时间长度。

2. 准确估量

为了提高性能分析的准确性,可以对 profile 模块的性能分析器进行校准:

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

校准后,可以通过以下方式应用计算出的偏差值:

# 方法 1:应用于所有后续创建的 Profile 实例
profile.Profile.bias = your_computed_bias

# 方法 2:应用于特定的 Profile 实例
pr = profile.Profile()
pr.bias = your_computed_bias

# 方法 3:在构造函数中指定
pr = profile.Profile(bias=your_computed_bias)

七、总结

掌握 Python 的 cProfileprofile 模块,能够让我们轻松地对程序进行性能分析,找出性能瓶颈并进行优化。在编程狮平台上,您可以进一步学习和实践这些性能分析工具,提升自己的 Python 编程技能,编写出更高效、更优质的代码。通过持续的性能分析和优化,让您的程序在各种场景下都能表现出色。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号