使用timeit和cProfile对Python代码进行性能分析

使用timeit和cProfile分析Python代码性能

作为一名软件开发人员,你可能听过这句话:“过早优化是万恶之源”,在你的职业生涯中不止一次。虽然优化对于小项目来说可能没有太大帮助(或绝对必要),但是分析是常常有帮助的。

在完成编写模块后,对代码进行分析来测量每个部分的执行时间是一个好的实践。这可以帮助识别代码异味并指导优化以提高代码质量。所以在优化之前,总是要对代码进行分析!

为了迈出第一步,本指南将帮助您开始使用Python进行分析,使用内置的timeit和cProfile模块。您将学习如何同时使用命令行界面和在Python脚本中等效的可调用函数。

如何使用timeit对Python代码进行分析

timeit模块是Python标准库的一部分,提供了一些便利函数,用于计时短代码片段。

让我们以一个简单的例子来说明,反转一个Python列表。我们将测量使用以下方法获取列表的反转副本的执行时间:

  • reversed()函数,和
  • 列表切片。
  >>> nums=[6,9,2,3,7] >>> list(reversed(nums)) [7, 3, 2, 9, 6] >>> nums[::-1] [7, 3, 2, 9, 6]  

在命令行中运行timeit

您可以使用以下语法在命令行中运行timeit

 $ python -m timeit -s 'setup-code' -n 'number' -r 'repeat' 'stmt' 

您需要提供要测量执行时间的语句stmt

您可以在需要时指定setup代码,使用短选项-s或长选项–setup。设置代码只会运行一次。

运行语句的次数:短选项-n或长选项–number是可选的。重复此循环的次数:短选项-r或长选项–repeat也是可选的。

让我们看看如何运用到我们的例子中:

这里创建列表是setup代码,反转列表是要计时的语句:

 $ python -m timeit -s 'nums=[6,9,2,3,7]' 'list(reversed(nums))' 500000 loops, best of 5: 695 nsec per loop 

当您不指定repeat的值时,默认值为5。当您不指定number时,代码将运行多次,以达到至少0.2秒的总时间。

这个例子明确设置了要执行该语句的次数:

 $ python -m timeit -s 'nums=[6,9,2,3,7]' -n 100Bu000 'list(reversed(nums))' 100000 loops, best of 5: 540 nsec per loop 

repeat的默认值是5,但我们可以将其设置为任何适当的值:

 $ python3 -m timeit -s 'nums=[6,9,2,3,7]' -r 3 'list(reversed(nums))' 500000 loops, best of 3: 663 nsec per loop 

让我们也计时列表切片的方法:

 $ python3 -m timeit -s 'nums=[6,9,2,3,7]' 'nums[::-1]' 1000000 loops, best of 5: 142 nsec per loop 

列表切片方法似乎更快(所有示例均在Ubuntu 22.04上的Python 3.10中进行)。

在Python脚本中运行timeit

下面是在Python脚本中运行timeit的等效代码:

import timeit

setup = 'nums=[9,2,3,7,6]'
number = 100000
stmt1 = 'list(reversed(nums))'
stmt2 = 'nums[::-1]'

t1 =  timeit.timeit(setup=setup, stmt=stmt1, number=number)
t2 = timeit.timeit(setup=setup, stmt=stmt2, number=number)

print(f"使用reversed()函数:{t1}")
print(f"使用列表切片:{t2}")

timeit()函数返回stmt的执行时间,执行number次。注意,我们可以显式地指定要运行的次数,或者让number取默认值1000000。

输出 >>
使用reversed()函数:0.08982690000000002
使用列表切片:0.015550800000000004

这将运行语句(不重复计时器函数)指定的次数number,并返回执行时间。通常还可以使用time.repeat()并取最小时间,如下所示:

import timeit

setup = 'nums=[9,2,3,7,6]'
number = 100000
stmt1 = 'list(reversed(nums))'
stmt2 = 'nums[::-1]'

t1 =  min(timeit.repeat(setup=setup, stmt=stmt1, number=number))
t2 = min(timeit.repeat(setup=setup, stmt=stmt2, number=number))

print(f"使用reversed()函数:{t1}")
print(f"使用列表切片:{t2}")

这将重复运行代码numberrepeat次,并返回最小执行时间。这里我们重复了5次,每次重复100000次。

输出 >>
使用reversed()函数:0.055375300000000016
使用列表切片:0.015101400000000043

如何使用cProfile对Python脚本进行性能分析

我们已经了解了如何使用timeit来测量短代码片段的执行时间。然而,在实践中,更有帮助的是对整个Python脚本进行性能分析。

这将给出所有函数和方法调用的执行时间,包括内置函数和方法。因此,我们可以更好地了解哪些函数调用更耗时,并找出优化的机会。例如:可能存在一个太慢的API调用。或者一个函数可能有一个可以用更Pythonic的推导表达式替代的循环。

让我们学习如何使用cProfile模块(也是Python标准库的一部分)对Python脚本进行性能分析。

考虑以下Python脚本:

# main.py
import time

def func(num):
    for i in range(num):
        print(i)

def another_func(num):
    time.sleep(num)
    print(f"睡眠了{num}秒")

def useful_func(nums, target):
    if target in nums:
        return nums.index(target)

if __name__ == "__main__":
    func(1000)
    another_func(20)
    useful_func([2, 8, 12, 4], 12)

这里有三个函数:

  • func()循环遍历数字范围并将其打印出来。
  • another func()包含对sleep()函数的调用。
  • useful_func()返回列表中目标数字的索引(如果目标存在于列表中)。

每次运行main.py脚本时,上述列出的函数都会被调用。

在命令行中运行cProfile

在命令行中使用以下命令运行cProfile:

python3 -m file-name.py

这里我们将文件命名为main.py:

python3 -m main.py

运行后,应该会得到以下输出:

  输出 >>
  0
  ...
  999
  睡眠了20秒

以及以下性能分析:

在这里,ncalls代表函数调用的次数,percall代表每次函数调用的时间。如果ncalls的值大于1,则percall是所有调用的平均时间。

脚本的执行时间主要由使用内置的sleep函数调用的another_func函数占据(睡眠20秒)。我们可以看到print函数调用也是相当昂贵的。

在Python脚本中使用cProfile

虽然在命令行中运行cProfile效果很好,但你也可以将性能分析功能添加到Python脚本中。你可以使用cProfile和pstats模块进行性能分析和访问统计信息。

为了更好地处理资源设置和清理,最佳实践是使用with语句并创建一个作为上下文管理器使用的性能分析对象:

# main.py
import pstats
import time
import cProfile

def func(num):
    for i in range(num):
        print(i)

def another_func(num):
    time.sleep(num)
    print(f"睡眠了{num}秒")

def useful_func(nums, target):
    if target in nums:
        return nums.index(target)


if __name__ == "__main__":
    with cProfile.Profile() as profile:
        func(1000)
        another_func(20)
        useful_func([2, 8, 12, 4], 12)
    profile_result = pstats.Stats(profile)
    profile_result.print_stats()

让我们仔细看一下生成的输出性能分析:

当你对一个大型脚本进行性能分析时,按执行时间对结果进行排序会很有帮助。为此,可以在性能分析对象上调用sort_stats,并根据执行时间进行排序:

...
if __name__ == "__main__":
    with cProfile.Profile() as profile:
        func(1000)
        another_func(20)
        useful_func([2, 8, 12, 4], 12)
    profile_result = pstats.Stats(profile)
    profile_result.sort_stats(pstats.SortKey.TIME)
    profile_result.print_stats()

现在运行脚本,你应该能够看到按时间排序的结果:

结论

希望本指南能帮助你开始在Python中进行性能分析。请记住,优化不应该以可读性为代价。如果你有兴趣了解其他性能分析工具,包括第三方Python包,请查看这篇关于Python性能分析工具的文章。Bala Priya C是来自印度的开发人员和技术作家。她喜欢在数学、编程、数据科学和内容创作的交叉领域工作。她感兴趣和擅长的领域包括DevOps、数据科学和自然语言处理。她喜欢阅读、写作、编码和咖啡!目前,她正在通过撰写教程、指南、观点文章等,向开发者社区学习和分享她的知识。