Python自带的调试及性能分析神器

2020-11-25 14:30:35 浏览数 (1)

阅读本文大概需要 5 分钟。

工具可以大大提升效率,节省时间,能自己掌控的时间越多,越自由。Python 也是效率工具,使用的越多,你省下来的时间就越多,你就会越觉得自由,因为一切尽在你的代码掌控中。

但有一种情况,可能会耗费你很多时间,那就是调试和性能分析,说到这里,有人可能会说,调试不就是 print 下变量,性能分析不就是加个耗时统计么,有多耗费时间呢?没错,简单的程序,怎么弄都是简单的,如果是复杂的程序,比如上千行的代码,无限多的调用,你还用简单的方法,我只能说你很有耐心。

今天为大家分享下 Python 标准库自带神器,一个是调试工具 pdb,一个是性能分析工具 cProfile,非常实用,如果不会这两个,真的太遗憾了。

使用 pdb 调试

先说下为什么用 pdb,假如你只会用 pycharm 或 vscode 的调试(debug)功能,现在让你直接在服务器对异常进行调试,没有任何图形界面的 IDE,只有 Python 环境及运行的代码,你怎么办?

此外,不少代码已经挪到了类似 Jupyter 的 Notebook 中,往往就要求开发者使用命令行的形式,来对代码进行调试。

所以,掌握通用技术才能通吃,而命令行的调试工具 pdb 就是通用的,掌握这个,无论什么环境都不影响你 debug。

接下来,我们就一起来看看,pdb 在 Python 中到底应该如何使用。首先,要启动 pdb 调试,我们只需要在程序中,加入“import pdb”和“pdb.set_trace()”这两行代码就行了,比如下面这个简单的例子:

代码语言:javascript复制
import pdb
for i in range(10000):
    print(i)
    if i == 800:
        pdb.set_trace()

当这个循环进行到 i==800 时,自动停下来进入命令行的调试,输入 i 即可查询变量的值,输入 n 表示执行下一行,输入 ll 查看上下文,输入 help 查看帮助。

代码语言:javascript复制
......
799
800
> /Users/aaronbrant/test.py(3)<module>()
-> for i in range(10000):
(Pdb) i
800
(Pdb) n
> /Users/aaronbrant/test.py(4)<module>()
-> print(i)
(Pdb) n
801
> /Users/aaronbrant/test.py(5)<module>()
-> if i == 800:
(Pdb) ll
  1      import pdb
  2
  3      for i in range(10000):
  4          print(i)
  5  ->        if i == 800:
  6              pdb.set_trace()
(Pdb) help

Documented commands (type help <topic>):
========================================
EOF    c          d        h         list      q        rv       undisplay
a      cl         debug    help      ll        quit     s        unt
alias  clear      disable  ignore    longlist  r        source   until
args   commands   display  interact  n         restart  step     up
b      condition  down     j         next      return   tbreak   w
break  cont       enable   jump      p         retval   u        whatis
bt     continue   exit     l         pp        run      unalias  where

如果使用 IDE,是否要点击 800 次呢,我不是很清楚,没试过,如果使用 IDE 来断点定位至循环内的 800 次,我会直接放弃,选用其他方式。

其他命令:

  • s 表示 step into,即进入相对应的代码内部。这时,命令行中会显示”--Call--“的字样,当你执行完内部的代码块后,命令行中则会出现”--Return--“的字样。
  • r 表示 step out,即继续执行,直到当前的函数完成返回。
  • b 可以用来设置断点。比方说,我想要在代码中的第 10 行,再加一个断点,那么在 pdb 模式下输入”b 11“即可。
  • c 则表示一直执行程序,直到遇到下一个断点。

当然,除了这些常用命令,还有许多其他的命令可以使用,这里我就不在一一赘述了。你可以参考对应的官方文档(https://docs.python.org/3/library/pdb.html#module-pdb),来熟悉这些用法。

使用 cProfile 进行性能分析

除了要对程序进行调试,性能分析也是每个开发者的必备技能。日常工作中,我们常常会遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟(latency)高,占用的资源多,但却不知道是哪里出了问题。这时,对代码进行 profile 就显得异常重要了。

这里所谓的 profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。这样你就可以知道程序的瓶颈所在,从而对其进行修正或优化。当然,这并不需要你花费特别大的力气,在 Python 中,这些需求用 cProfile 就可以实现。

举个例子,比如我想计算斐波拉契数列,运用递归思想,我们很容易就能写出下面这样的代码

代码语言:javascript复制
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1)   fib(n-2)

def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n-1))
    res.append(fib(n))
    return res

print(fib_seq(30))

接下来,我想要测试一下这段代码总的效率以及各个部分的效率。那么,我就只需在开头导入 cProfile 这个模块,并且在最后运行 cProfile.run() 就可以了,如下所示:

代码语言:javascript复制
import cProfile
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1)   fib(n-2)

def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n-1))
    res.append(fib(n))
    return res

cProfile.run('fib_seq(30)')

或者更简单一些,直接在运行脚本的命令中,加入选项“-m cProfile”也很方便:

代码语言:javascript复制
python -m cProfile test.py

运行结果如下:

代码语言:javascript复制
(py37env) ➜  ~ python test.py
         7049218 function calls (96 primitive calls) in 1.503 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    1.503    1.503 <string>:1(<module>)
     31/1    0.000    0.000    1.503    1.503 test.py:10(fib_seq)
7049123/31    1.503    0.000    1.503    0.048 test.py:2(fib)
        1    0.000    0.000    1.503    1.503 {built-in method builtins.exec}
       31    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       30    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}

这里有一些参数你可能比较陌生,我来简单介绍一下:

  • ncalls,是指相应代码 / 函数被调用的次数;
  • tottime,是指对应代码 / 函数总共执行所需要的时间(注意,并不包括它调用的其他代码 / 函数的执行时间);
  • tottime percall,就是上述两者相除的结果,也就是tottime / ncalls;
  • cumtime,则是指对应代码 / 函数总共执行所需要的时间,这里包括了它调用的其他代码 / 函数的执行时间;
  • cumtime percall,则是 cumtime 和 ncalls 相除的平均结果。

了解这些参数后,再来看运行结果。我们可以清晰地看到,这段程序执行效率的瓶颈,在于第二行的函数 fib(),它被调用了 700 多万次。

有没有什么办法可以提高改进呢?答案是肯定的。通过观察,我们发现,程序中有很多对 fib() 的调用,其实是重复的,那我们就可以用字典来保存计算过的结果,防止重复。改进后的代码如下所示:

代码语言:javascript复制
def memoize(f):
    memo = {}
    def helper(x):
        if x not in memo:
            memo[x] = f(x)
        return memo[x]
    return helper

@memoize
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1)   fib(n-2)


def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n-1))
    res.append(fib(n))
    return res

fib_seq(30)

上述代码保存为 test2.py,直接在命令行执行,结果如下:

代码语言:javascript复制
(py37env) ➜  ~ python -m cProfile test2.py
         216 function calls (128 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 test2.py:1(<module>)
        1    0.000    0.000    0.000    0.000 test2.py:1(memoize)
     31/1    0.000    0.000    0.000    0.000 test2.py:19(fib_seq)
    89/31    0.000    0.000    0.000    0.000 test2.py:3(helper)
       31    0.000    0.000    0.000    0.000 test2.py:9(fib)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
       31    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       30    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}

可以看出效率得到了极大的提升。

这只是个简单的例子,便是 cProfile 的基本用法,当然,cProfile 还有很多其他功能,还可以结合 stats 类来使用,你可以阅读相应的官方文档来了解。

小结

孰能生巧,pdb 是 Python 常用的调试工具,cProfile 是经典的性能分析工具,在 debug 及性能分析方面,可以大大提升你的效率。文章内容整理自极客时间专栏,如果觉得对你有帮助,希望系统的学习 Python 技术,欢迎扫下方二维码订阅,并加我微信好友,可以得到返现哦。

0 人点赞