写代码怎样才能如鱼得水
写代码怎样才能如鱼得水取决于你写代码的态度,首先很多人在写代码的过程中可能真的只注重了写代码,没有过多的思考,也不会对代码进行调试或性能上的分析,仅仅是为了完成或实现某个功能而去写代码,这样的写代码是没有灵魂的,你不会得到丝毫的进步,长久下去也只能沦为码农而不是工程师。
那么如何才能做到是事半功倍的编写代码,并能在每一次的开发中得到提升和进步呢?其实说起来也不难,那就是每次对编写的代码进行调试和性能分析,并深度思考进行程序优化提升,以此来提升你的编写能力,冰冻三尺非一日之寒,这事情虽然简单但是却需要强大的执行力。但如果不想沦为码农或成为社畜,就需要你的行动了。接下来就为大家分享一下如何对代码进行调试和性能分析助你一臂之力。
代码调试
说到代码调试,很多同学会问,那还不简单吗?print()不就可以了吗?你说的对,我们会在开发的过程中大量使用print来对代码进行一些输出测试,以确保程序是否达到预期,这是一种非常简单粗暴的快速debug的方式,但是这种方法也仅限于小型程序。
我们会对每一个代码块进行print输出来调试是否达到预期,但是如果程序比较大,每次都需要重新运行才能看到打印的结果,那成本肯定就很高,特别还有一些通常需要反复运行调试才能找到错误的根源,这样如果仅仅使用print()打印那效率就很低了。
当然,除了使用print之外,我们还可以借助于IDE来完成debug。比如Pycharm,就可以在代码中设置断点,这样只要程序运行到断点就会自动停止,并可以方便的查看变量的值,当然也推荐大家使用这样的方式进行debug。
但是也有一些情况是Pycharm无法满足的,比如Pycharm只能针对本项目中的py代码进行调试,另外很多进行数据分析、数据挖掘和AI算法的代码其实大多数都是在 Jupyter 的 Notebook 中。那么这种情况就下Pycharm就无能无力了。因此今天我们为大家分享一个Python中自带的高效的代码调试库pdb,那么它是什么?又如何使用呢?
使用pdb进行代码调试
pdb是python自带的代码调试库,是一个交互式源代码调试器。它支持在源代码行级别设置断点和单步执行、堆栈帧检查、源代码列表以及在任何堆栈帧的上下文中评估任意Python代码。
Python官方文档地址:https://docs.python.org/3/library/pdb.html#module-pdb
如何使用 pdb
要启动 pdb 调试,我们只需要在程序中,加入import pdb
和pdb.set_trace()
这两行代码就行了
a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)
接下来我们运行这个代码,它的输出界面是下面这样的,表示程序已经运行到了pdb.set_trace()
这行,并且暂停了下来,等待输入指令。
> /Users/yc/Desktop/code/demo.py(5)<module>()
-> c = 3
(Pdb)
这时,我们就可以执行一些调试,比如打印变量的语法是p <expression>
(pdb) p a # 输入 p a 等于要查看变量a的值
1
(pdb) p b
2
(pdb) p c # 因为程序目前只运行了前面几行,此时的变量 c 还没有被定义:
*** NameError: name 'c' is not defined
如果需要程序继续执行下一行可以输入n
,表示继续执行代码到下一行
(pdb) n
-> print(a + b + c)
查看当前代码行附近的源代码可以输入l
,列出当前代码行上下的 11 行源代码
(pdb) l
1 a = 1
2 b = 2
3 import pdb
4 pdb.set_trace()
5 -> c = 3
6 print(a + b + c)
pdb常用指令参考
完整指令 | 简写指令 | 指令描述 |
---|---|---|
break | b <行号> | 在某一行设置断点 |
break | b <文件名>:<行号> | 在某个文件的某行打一点断点 |
break | b <函数名> | 在某个含税的第一行打一个断点 |
clear | cl | 清除所有断点 |
clear | cl n1 n2 | 清除编号为n1、n2的断点 |
next | n | 执行下一条语句,遇到函数不进入其内部 |
step | s | 执行下一条语句,遇到函数会进入其内部 |
continue | c或者cont | 执行到下一个断点 |
list | l | 列出源码(前后11行代码) |
list | l <行号> | 列出某行周围11行代码(即这一行为中间行,列出它上下各5行) |
list | l <行号1> <行号2> | 列出两个行号范围内的代码 |
p | p | 打印变量值,也可以用print |
pp | pp | 好看一点的输出 |
quit | q | 退出 pdb |
return | r | 一直运行到函数返回 |
以上就是给大家做了一个简单的pdb使用方法,除了这些常用命令,还有许多其他的命令可以使用,这里我就不在一一赘述了。可以参考对应的官方文档,来熟悉这些用法。
用 cProfile 进行性能分析
那么除了要对程序进行调试,性能分析也是每个开发者的必备技能。
日常工作中,我们常常会遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟(latency)高,占用的资源多,但却不知道是哪里出了问题。这时,对代码进行性能分析就显得非常重要,通过性能分析你就可以知道程序的瓶颈所在,从而对其进行修正或优化。当然,这并不需要你花费特别大的力气,在 Python 中,这些需求用 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
fib_seq(30)
接下来,我想要测试一下这段代码总的效率以及各个部分的效率。
那么,我就只需在开头导入 cProfile 这个模块,并且在最后运行 cProfile.run() 就可以了:
import cProfile
def fib(n):...
def fib_seq(n):...
cProfile.run('fib_seq(30)')
或者更简单一些,直接在运行脚本的命令中,加入选项“-m cProfile”也很方便:
python3 -m cProfile xxx.py
运行完毕后,我们可以看到下面这个输出界面:
7049218 function calls (96 primitive calls) in 2.280 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 2.280 2.280 <string>:1(<module>)
31/1 0.000 0.000 2.280 2.280 demo.py:10(fib_seq)
7049123/31 2.280 0.000 2.280 0.074 demo.py:3(fib)
1 0.000 0.000 2.280 2.280 {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}
输出的第一行
7049218 function calls (96 primitive calls) in 2.280 seconds
是指本次运行总计7049218个函数调用被监控,其中96个是原生调用(不涉及递归)
其它列信息:
ncalls,是指相应代码 / 函数被调用的次数;
tottime,是指对应代码 / 函数总共执行所需要的时间(注意,并不包括它调用的其他代码 / 函数的执行时间);
- tottime percall,就是上述两者相除的结果,也就是tottime / ncalls;
- cumtime,则是指对应代码 / 函数总共执行所需要的时间,这里包括了它调用的其他代码 / 函数的执行时
- cumtime percall,则是 cumtime 和 ncalls 相除的平均结果。
了解这些参数后,再来看这个结果。我们可以清晰地看到,这段程序执行效率的瓶颈,在于第二行的函数 fib(),它被调用了 700 多万次。
有没有什么办法可以提高改进呢?答案是肯定的。通过观察,我们发现,程序中有很多对 fib() 的调用,其实是重复的,那我们就可以用字典来保存计算过的结果,防止重复。
改进后的代码如下所示:
def funcopt(f):
opt = {}
def helper(x):
if x not in opt:
opt[x] = f(x)
return opt[x]
return helper
@funcopt
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)
这时,我们再对其进行 profile
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 chuange.py:1(<module>)
1 0.000 0.000 0.000 0.000 chuange.py:1(funcopt)
31/1 0.000 0.000 0.000 0.000 chuange.py:16(fib_seq)
89/31 0.000 0.000 0.000 0.000 chuange.py:3(helper)
31 0.000 0.000 0.000 0.000 chuange.py:8(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}
此时你会看到由原来的700万次的函数调用优化到了216次,性能的提高自然就不用多说了。
这个简单的例子,便是 cProfile 的基本用法,也是我今天分享的重点。当然,cProfile 还有很多其他功能,还可以结合 stats 类来使用,你可以阅读相应的 官方文档 来了解。
总结
本章节我们为大家分享了 Python 中常用的调试工具 pdb,和经典的性能分析工具 cProfile。pdb 为 Python 程序提供了一种通用的、交互式的高效率调试方案;而 cProfile 则是为开发者提供了每个代码块执行效率的详细分析,有助于我们对程序的优化与提高。关于它们的更多用法,你可以通过它们的官方文档进行实践,熟能生巧。
最后希望各位小伙伴们写代码事半功倍,如鱼得水。
如果喜欢或者对你有帮助的小伙伴,欢迎大家关注我的公众号:后厂程序员,并分享、点赞、在看 三连