使用 timeit 和 cProfile 分析 Python 代码 - KDnuggets

使用 timeit 和 cProfile 分析 Python 代码 – KDnuggets

源节点: 2874649

使用 timeit 和 cProfile 分析 Python 代码
图片作者
 

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

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

为了迈出第一步,本指南将帮助您开始使用 Python 进行分析——使用内置的 时间个人资料 模块。 您将学习如何使用命令行界面和 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。 设置代码将仅运行一次。

number 运行该语句的次数:短选项 -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 3.10 上的 Python 22.04 中)。

在 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}")

 

timeit() callable 返回执行时间 stmt number 次。 请注意,我们可以明确提及要运行的次数,或者使 number 取默认值 1000000。

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

 

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

Output >>
Using reversed() fn.: 0.055375300000000016
Using list slicing: 0.015101400000000043

我们已经了解了如何使用 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)

 

这里我们有三个函数:

  • func() 循环遍历一系列数字并将其打印出来。
  • another func() 其中包含对 sleep() 功能。
  • useful_func() 返回列表中目标编号的索引(如果目标存在于列表中)。

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

在命令行运行 cProfile

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

python3 -m file-name.py

 

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

python3 -m main.py

 

运行它应该会给出以下输出:

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

 

以及以下简介:

 

使用 timeit 和 cProfile 分析 Python 代码
 

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

脚本的执行时间主要由 another_func 使用内置的 sleep 函数调用(休眠 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"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()

 

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

 

使用 timeit 和 cProfile 分析 Python 代码
 

当您分析大型脚本时,这会很有帮助 按执行时间对结果进行排序。 为此,您可以致电 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()

 

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

 

使用 timeit 和 cProfile 分析 Python 代码

我希望本指南可以帮助您开始使用 Python 进行分析。 永远记住,优化永远不应该以牺牲可读性为代价。 如果您有兴趣了解其他分析器,包括第三方 Python 包,请查看此内容 关于 Python 分析器的文章.
 
 
巴拉普里亚 C 是来自印度的开发人员和技术作家。 她喜欢在数学、编程、数据科学和内容创作的交叉领域工作。 她的兴趣和专长领域包括 DevOps、数据科学和自然语言处理。 她喜欢阅读、写作、编码和咖啡! 目前,她致力于通过编写教程、操作指南、评论文章等方式学习并与开发人员社区分享她的知识。
 

时间戳记:

更多来自 掘金队