17.1 为什么要调试
- 代码本身有问题,需要我们找到 root cause 并修复
- 代码效率有问题,比如过度浪费资源,latency很大,因此需要我们 debug
- 在开发新的 feature 时,一般都需要测试
- 阅读开源项目时,通过调试了解处理逻辑
17.2 调试方法
17.2.1 print调试法
对于简单的程序,使用print函数输出可以进行调试。但是对于大型程序,怀疑出错的地方可能很多,就可能代码中需要写大量的print函数。调试完毕后,还要再删除掉。显然比较麻烦。
17.2.2 Pycharm断点
Pycharm这个好用的Python集成开发环境,提供断点调试功能。在怀疑出错的地方添加断点,再通过Debug方法执行程序,程序会停止在断点处,相比使用print函数,大大提高了效率。
添加断点的方法是在Pycharm行号右侧,鼠标左键点击一下就可以了,这样程序通过Debug执行到这里就会停住。还可以给断点设置条件,就是当满足一定条件才停住,方法是在红色断点圆点上点击右键,在Condition栏里面添加条件,再点击Done之后,发现圆点右下角有一个问号了,表示这是一个条件断点。
在Pycharm上点击Debug按钮,可以开始调试程序,会在Pycharm下显示Debugger的调试窗口。这个窗口有三个主要部分组成:最左边是调用栈信息Frames,中间是Varibles信息,最右边是Watches信息,这个窗口可以将我们感兴趣的Varibles通过+号添加进来。
当程序停住后,可以通过Varibles窗口上方的一排按钮控制程序的执行。
最左边的,表示让程序继续执行一行,左边第二个是进入当前行中的函数内不,从右边数第二个是从函数中跳出到被调用处的位置,右数第一个是执行到光标所在行。当通过这些按钮控制程序执行时,Frames窗口,Varibles窗口,Watches窗口信息会发生变化。我们就是通过这些信息来了解代码的执行中间过程的。
17.2.3 pdb调试法
pdb 是 Python 程序自带的一种用于交互式调试的包。需要在程序中,加入“import pdb”和“pdb.set_trace()”这两行代码。pdb一共有几个常见的命令:p、n、l、s,可以结合下面的代码学习:
import pdb
a = 1
b = 2
pdb.set_trace() # 暂停了下来,等待用户输入
def echo(x):
print(x)
c = 3
echo(a + b + c)
执行上面的代码,我分别使用上面的四个命令,请看注释。
/usr/local/bin/python3.7 /Users/chunming.liu/PycharmProjects/mypytest/test/test_suite1/func.py
> /Users/chunming.liu/PycharmProjects/mypytest/test/test_suite1/func.py(8)<module>()
-> def echo(x): # 运行到这一行停止
(Pdb) p a # 打印变量a
1
(Pdb) n # 执行下一行
> /Users/chunming.liu/PycharmProjects/mypytest/test/test_suite1/func.py(12)<module>()
-> c = 3
(Pdb) l # 列出当前代码行前后11行代码
7
8 def echo(x):
9 print(x)
10
11
12 -> c = 3
13 echo(a + b + c)
[EOF]
(Pdb) n # 执行下一行
> /Users/chunming.liu/PycharmProjects/mypytest/test/test_suite1/func.py(13)<module>()
-> echo(a + b + c)
(Pdb) s # 进入到echo函数中
--Call--
> /Users/chunming.liu/PycharmProjects/mypytest/test/test_suite1/func.py(8)echo()
-> def echo(x):
(Pdb) p x
6
(Pdb) n
> /Users/chunming.liu/PycharmProjects/mypytest/test/test_suite1/func.py(9)echo()
-> print(x)
(Pdb) n
6
--Return--
> /Users/chunming.liu/PycharmProjects/mypytest/test/test_suite1/func.py(9)echo()->None
-> print(x)
(Pdb)
使用命令s进入了函数 echo() 的内部,显示–Call–;而当我们执行完函数 echo() 内部语句并跳出后,显示–Return–。另外,还可以使用b 11
在第11行设置断点,使用c
运行到下一个断点。
17.3 性能分析方法
cProfile 提供了每个代码块执行效率的详细分析。
cProfile 计算出每个模块消耗的时间,这样就可以知道程序的瓶颈所在,从而对其进行修正或优化。
下面是一个输出Fibonacci序列的函数,用cProfile分析性能:
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)') # 分析哪个函数,就将那个函数作为cProfile.run的参数
将会输出,性能分析数据:
7049218 function calls (96 primitive calls) in 2.519 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 2.519 2.519 <string>:1(<module>)
31/1 0.000 0.000 2.519 2.519 func.py:13(fib_seq)
7049123/31 2.519 0.000 2.519 0.081 func.py:4(fib)
1 0.000 0.000 2.519 2.519 {
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() 的调用,其实是有很多重复的,那我们就可以用字典来保存计算过的结果,防止重复计算。利用装饰器优化上面的代码:
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)
这次的性能输出如下,可见fib函数的调用次数只剩下31次了,大大提高了代码执行效率:
215 function calls (127 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 <string>:1(<module>)
31 0.000 0.000 0.000 0.000 func.py:15(fib)
31/1 0.000 0.000 0.000 0.000 func.py:25(fib_seq)
89/31 0.000 0.000 0.000 0.000 func.py:7(helper)
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}