使用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}")
这将重复运行代码number
次repeat
次,并返回最小执行时间。这里我们重复了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、数据科学和自然语言处理。她喜欢阅读、写作、编码和咖啡!目前,她正在通过撰写教程、指南、观点文章等,向开发者社区学习和分享她的知识。