7.1 KiB
Python 102:如何剖析您的代码
原文:https://www.blog.pythonlibrary.org/2014/03/20/python-102-how-to-profile-your-code/
代码分析是试图在代码中找到瓶颈。剖析应该是为了找出代码的哪些部分耗时最长。一旦你知道了这一点,你就可以看看你的代码,并试图找到优化它的方法。Python 内置了三个分析器: cProfile 、 profile 和 hotshot 。根据 Python 文档, hotshot “不再维护,可能会在 Python 的未来版本中被删除”。 profile 模块是一个纯粹的 Python 模块,但是给被分析的程序增加了很多开销。因此,我们将关注于 cProfile ,它有一个模拟 Profile 模块的接口。
用概要文件分析代码
用 cProfile 分析代码真的很容易。你需要做的就是导入模块并调用它的 run 函数。让我们看一个简单的例子:
>>> import hashlib
>>> import cProfile
>>> cProfile.run("hashlib.md5('abcdefghijkl').digest()")
4 function calls in 0.000 CPU seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :1(<module>)
1 0.000 0.000 0.000 0.000 {_hashlib.openssl_md5}
1 0.000 0.000 0.000 0.000 {method 'digest' of '_hashlib.HASH' objects}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
这里我们导入了 hashlib 模块,并使用 cProfile 来分析 MD5 散列的创建。第一行显示有 4 个函数调用。下一行告诉我们结果是如何排序的。根据文献记载,标准名称是指最右边的列。这里有许多列。
- ncalls 是发出的呼叫数。
- tottime 是给定函数花费的总时间。
- percall 指总时间除以 ncalls 的商
- 累计时间是在该功能和所有子功能中花费的累计时间。甚至对递归函数也很准确!
- 第二个 percall 列是累计时间除以原始调用的商
- **filename:line no(function)**提供每个函数各自的数据
原始调用不是通过递归引起的。
这不是一个非常有趣的例子,因为没有明显的瓶颈。让我们创建一段带有内置瓶颈的代码,看看分析器是否能检测到它们。
import time
#----------------------------------------------------------------------
def fast():
""""""
print("I run fast!")
#----------------------------------------------------------------------
def slow():
""""""
time.sleep(3)
print("I run slow!")
#----------------------------------------------------------------------
def medium():
""""""
time.sleep(0.5)
print("I run a little slowly...")
#----------------------------------------------------------------------
def main():
""""""
fast()
slow()
medium()
if __name__ == '__main__':
main()
在这个例子中,我们创建了四个函数。前三种以不同的速度运行。快速功能将以正常速度运行;中功能大约需要半秒钟运行,慢功能大约需要 3 秒钟运行。主函数调用其他三个。现在让我们对这个愚蠢的小程序运行 cProfile:
>>> import cProfile
>>> import ptest
>>> cProfile.run('ptest.main()')
I run fast!
I run slow!
I run a little slowly...
8 function calls in 3.500 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 3.500 3.500 :1(<module>)
1 0.000 0.000 0.500 0.500 ptest.py:15(medium)
1 0.000 0.000 3.500 3.500 ptest.py:21(main)
1 0.000 0.000 0.000 0.000 ptest.py:4(fast)
1 0.000 0.000 3.000 3.000 ptest.py:9(slow)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
2 3.499 1.750 3.499 1.750 {time.sleep}
这一次,我们看到程序运行了 3.5 秒。如果您检查结果,您将看到 cProfile 已经将慢速函数标识为花费 3 秒来运行。那是继主功能之后的最大瓶颈。通常,当您发现类似这样的瓶颈时,您会试图找到一种更快的方法来执行您的代码,或者可能决定运行时是可以接受的。在这个例子中,我们知道加速这个函数的最好方法是删除 time.sleep 调用或者至少减少睡眠时间。
您也可以在命令行上调用 cProfile,而不是在解释器中使用它。有一种方法可以做到:
python -m cProfile ptest.py
这将按照与我们之前相同的方式对您的脚本运行 cProfile。但是,如果您想保存分析器的输出,该怎么办呢?嗯,使用 cProfile 很简单!你需要做的就是给它传递 -o 命令,后跟输出文件的名称(或路径)。这里有一个例子:
python -m cProfile -o output.txt ptest.py
不幸的是,它输出的文件不完全是人类可读的。如果你想读取这个文件,那么你需要使用 Python 的 pstats 模块。您可以使用 pstats 以各种方式格式化输出。下面是一些代码,展示了如何获得一些类似于我们目前所看到的输出:
>>> import pstats
>>> p = pstats.Stats("output.txt")
>>> p.strip_dirs().sort_stats(-1).print_stats()
Thu Mar 20 18:32:16 2014 output.txt
8 function calls in 3.501 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 3.501 3.501 ptest.py:1()
1 0.001 0.001 0.500 0.500 ptest.py:15(medium)
1 0.000 0.000 3.501 3.501 ptest.py:21(main)
1 0.001 0.001 0.001 0.001 ptest.py:4(fast)
1 0.001 0.001 3.000 3.000 ptest.py:9(slow)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
2 3.499 1.750 3.499 1.750 {time.sleep}
<pstats.stats instance="" at=""></pstats.stats>
strip_dirs 调用将从输出中去除所有到模块的路径,而 sort_stats 调用进行我们习惯看到的排序。在 cProfile 文档中有许多非常有趣的例子,展示了使用 pstats 模块提取信息的不同方法。
包扎
此时,您应该能够使用 cProfile 模块来帮助您诊断代码如此缓慢的原因。您可能还想看看 Python 的 timeit 模块。如果您不想处理复杂的概要分析,它允许您对代码的小部分进行计时。还有其他几个第三方模块也很适合进行概要分析,比如 line_profiler 和 memory_profiler 项目。
相关阅读
- 关于概要模块的 Python 文档
- 如何用 timeit 为小块 Python 代码计时
- Python 性能分析指南
- PyMOTW: profile,cProfile,pstats