geekdoc-python-zh/docs/pythonlibrary/python-102-how-to-profile-y...

157 lines
7.1 KiB
Markdown
Raw Permalink Normal View History

2024-10-20 12:24:46 +08:00
# 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** 函数。让我们看一个简单的例子:
```py
>>> 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)**提供每个函数各自的数据
原始调用不是通过递归引起的。
这不是一个非常有趣的例子,因为没有明显的瓶颈。让我们创建一段带有内置瓶颈的代码,看看分析器是否能检测到它们。
```py
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:
```py
>>> 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而不是在解释器中使用它。有一种方法可以做到:
```py
python -m cProfile ptest.py
```
这将按照与我们之前相同的方式对您的脚本运行 cProfile。但是如果您想保存分析器的输出该怎么办呢使用 cProfile 很简单!你需要做的就是给它传递 **-o** 命令,后跟输出文件的名称(或路径)。这里有一个例子:
```py
python -m cProfile -o output.txt ptest.py
```
不幸的是,它输出的文件不完全是人类可读的。如果你想读取这个文件,那么你需要使用 Python 的 **pstats** 模块。您可以使用 pstats 以各种方式格式化输出。下面是一些代码,展示了如何获得一些类似于我们目前所看到的输出:
```py
>>> 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](https://pythonhosted.org/line_profiler/) 和 [memory_profiler](https://github.com/fabianp/memory_profiler) 项目。
* * *
### 相关阅读
* 关于[概要模块](http://docs.python.org/2/library/profile.html)的 Python 文档
* [如何用 timeit 为小块 Python 代码计时](https://www.blog.pythonlibrary.org/2014/01/30/how-to-time-small-pieces-of-python-code-with-timeit/)
* [Python 性能分析指南](http://www.huyng.com/posts/python-performance-analysis/)
* PyMOTW: [profilecProfilepstats](http://pymotw.com/2/profile/)