使用timeit和cProfile分析Python代码

2023年09月13日 由 alex 发表 302 0

如何使用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。


你可以在需要时指定设置代码 -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


当你不指定重复次数时,默认值为5。当你不指定次数时,代码将运行足够多的次数,以至少达到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


列表切片方法似乎更快。


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"Using reversed() fn.: {t1}")
print(f"Using list slicing: {t2}")


该可调用函数返回for of timestimeit()的执行时间。注意,我们可以显式地指定要运行的次数,或者让number采用默认值10万。


Output >>
Using reversed() fn.: 0.08982690000000002
Using list slicing: 0.015550800000000004


这将运行语句(不重复计时器函数)指定的次数,并返回执行时间。使用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"Using reversed() fn.: {t1}")
print(f"Using list slicing: {t2}")


这将重复运行代码的过程,重复次数,并返回最小执行时间。这里我们有5次重复,每次10万次。


Output >>
Using reversed() fn.: 0.055375300000000016
Using list slicing: 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"Slept for {num} seconds")
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)



这里有三个函数:


1. func() 循环遍历一系列数字并打印出来。


2. another func() 包含对sleep()函数的调用。


3. useful_func() 返回列表中目标数字的索引(如果目标存在于列表中)。


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


在命令行上运行cProfile


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


python3 -m file-name.py


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


python3 -m main.py


运行该命令会得到以下输出:


  Output >>
  0
  ...
  999
  Slept for 20 seconds


下面的简介:


3-1


在这里,ncalls指的是调用该函数的次数,percall指的是每次函数调用的时间。如果ncalls的值大于1,那么percall就是所有调用中的平均时间。


脚本的执行时间主要由另一个使用内置的sleep函数调用(休眠20秒)的函数another_func所占据。我们还可以看到print函数调用也很耗费时间。


使用Python脚本中的cProfile


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


作为处理资源设置和拆除的最佳实践,使用with语句并创建一个作为上下文管理器使用的profile对象。


# 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"Slept for {num} seconds")
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()


让我们仔细看看生成的输出配置文件:


3-2


当你对一个大型脚本进行分析时,按执行时间对结果进行排序是很有帮助的。要做到这一点,你可以在profile对象上调用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()


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


3-3


总结


希望本文能帮助你在Python中开始进行性能分析。优化永远不应该以牺牲可读性为代价。





文章来源:https://www.kdnuggets.com/profiling-python-code-using-timeit-and-cprofile
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消