第27章-代码分析

代码分析是一种在代码中查找瓶颈的尝试。分析是为了找出代码中最长的部分。一旦你知道了这一点,你就可以看看你的代码中的那些部分,并试图找到优化它的方法。python内置三个配置文件: cProfile轮廓热点 .根据python文档,hotshot“不再维护,可能会在将来的python版本中删除”。这个 轮廓 模块是一个纯Python模块,但是会给概要程序增加很多开销。因此,我们将专注于 cProfile ,它有一个模拟配置文件模块的接口。

用cprofile分析代码

用cprofile分析代码非常简单。您只需导入模块并调用它的 run 功能。让我们来看一个简单的例子:

>>> import hashlib
>>> import cProfile
>>> cProfile.run("hashlib.md5(b'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 <string>: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个函数调用。下一行告诉我们结果是如何排序的。根据文档,标准名称是指最右边的列。这里有许多列。

  • NCALL 是拨打的电话数。

  • 总计时间 是在给定函数中花费的总时间。

  • 佩雷尔 指tottime除以ncall的商。

  • 累积时间 是在此子函数和所有子函数中花费的累计时间。它对于递归函数甚至是精确的!

  • 第二 佩雷尔 column是cumtime除以基元调用的商

  • 文件名:lineno(函数) 提供每个函数的各自数据


原始调用不是通过递归诱导的。

这不是一个很有趣的例子,因为没有明显的瓶颈。让我们创建一段带有一些内置瓶颈的代码,看看分析器是否检测到它们。

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()

在这个例子中,我们创建了四个函数。前三个以不同的速率运行。这个 fast 功能将以正常速度运行;该 中等的 函数运行大约需要半秒钟,并且 slow 功能运行大约需要3秒钟。这个 main 函数调用其他三个。现在让我们来运行这个愚蠢的小程序的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 <string>: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已识别出 slow 运行时间为3秒。这是继 main 功能。通常,当您发现这样的瓶颈时,您会尝试寻找一种更快的方法来执行代码,或者决定运行时是可接受的。在这个例子中,我们知道加速函数的最佳方法是删除 time.sleep 打电话或者至少缩短睡眠时间。

您也可以在命令行上调用cprofile,而不是在解释器中使用它。有一种方法可以做到:

python -m cProfile ptest.py

这将对脚本运行cprofile,方式与我们之前所做的大致相同。但是如果您想保存分析器的输出呢?嗯,用CProfile很容易!你所要做的就是通过 -o 命令,后跟输出文件的名称(或路径)。下面是一个例子:

python -m cProfile -o output.txt ptest.py

不幸的是,它输出的文件不完全是人类可读的。如果您想读取该文件,那么需要使用python的 公共电话交换机 模块。您可以使用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(<module>)
        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 0x017C9030>

这个 strip_dirs 调用将从输出中删除到模块的所有路径,而 sort_stats call执行我们以前看到的排序。CProfile文档中有许多非常有趣的例子,展示了使用pstats模块提取信息的不同方法。

总结

此时,您应该能够使用 cProfile 模块来帮助您诊断代码为什么这么慢。您可能还想看看python的 timeit 模块。如果您不想处理与分析相关的复杂性,它允许您对代码的小部分进行计时。还有其他几个适合分析的第三方模块,如Line_Profiler和Memory_Profiler项目。