在码农群体中,有句话叫写代码就是写 bug,项目程序能一次写完并正常运行的概率几乎为 0,当然简单的 demo 除外。程序代码总会有各种各样的 bug 需要修正,有些 bug 很简单,看看错误信息就知道,有些 bug 很复杂,对于复杂的 bug,我们需要一整套调试程序的手段来找到它。
对于这种排查 bug 的方法有三种主流方式:一种方式是我们用 print 把可能出问题的变量打印到屏幕上,方便我们肉眼看到; 还有一种方式是通过调试工具单步运行,把程序执行动作放慢,我们可以观察每一步程序执行的过程, 这样我们不但能看到变量在内存中的值,还可以看到函数调用堆栈; 第三种方式是输出日志到磁盘文件进行记录,这样对于服务器程序用的比较多, 一个服务程序需要长期运行着,有可能前半年都没有出问题,后面某一天突然出现了问题, 我们不可能在程序运行的一开始就实时的去 print 或者去调试半年,我可以把可能会出现问题的地方做日志记录(输出到磁盘)方便我们在出问题后去查找。
本节课我们就学习以上三种方式,重点我们学习调试方式。
假定有一道题:写一个函数,查找给定 mylist 中的最大值,并返回一个 dict,返回值的样式为:{"max": max}。 下面是给出我们一开始写出的实现代码。
''' 算法思想: 1.先假定 mylist 中第一个元素为最大值,并把该值存储在存放最大值的变量中。 2.遍历 mylist,取出的元素和存储最大值的变量作比较,如果该元素大于存储最大值的变量,则把该元素赋值给存储最大值的变量。 3.一直遍历到最后一个元素,并返回 maxdict。 ''' def findmax(mylist): max = mylist[0] # 先假定第一个元素是最大值 maxdict = {"max": max} for item in mylist: if item > max: max = item return maxdict mylist = [4, 8, 9, 3] print(findmax(mylist))
最后我们发现打印出的值为:{"max": 4},这是怎么回事呢,我们程序的语法虽然没有什么问题, 就有可能是我们程序逻辑的问题了,这个时候我们可能会想,是不是我们的算法有问题,我们就会想到,在循环中每次打印出 max 的具体值,来查找程序所出现的问题。
def findmax(mylist): max = mylist[0] # 先假定第一个元素是最大值 maxdict = {"max": max} for item in mylist: if item > max: max = item print(max) # 8,9 print(maxdict) # {'max': 4},{'max': 4} return maxdict mylist = [4, 8, 9, 3] print(findmax(mylist))
通过打印,我们发现我们的算法没有问题,max 变量每次在循环内判断语句后的值都符合逻辑,但是 maxdict
的值并没有变化,由此我们知道,maxdict 的 "max" 键对应的值 max 和我们变量 max 不在同一块内存。
通过我们的思考和回忆,我们终于迷雾顿开,这个问题可以变化为:a = 4,b = a,a = 8,
b 还是 4,a的变化并没有改变 b 的值一个道理
。我们把代码修改成如下,正确解决 bug。
def findmax(mylist): max = mylist[0] # 先假定第一个元素是最大值 maxdict = {"max": max} for item in mylist: if item > maxdict["max"]: maxdict["max"] = item return maxdict mylist = [4, 8, 9, 3] print(findmax(mylist))
通过 print 方式来查找程序的 bug,并不是很直观,特别是在程序比较复杂的时候,函数调用层次比较深, 循环次数比较多等等。如果有一种方法可以看到程序每一步执行的顺序,变量在内存中的值,以及函数调用的堆栈,这样程序执行的逻辑就可以在视觉上清晰的反馈给码农, 这种查找程序问题的方式叫调试,Pycharm IDE 集成了调试器,我们可以很方便的对程序进行调试。 调试的第一步就是下断点,我们用鼠标在代码的左侧点击左键就可以下断点,如果下图。
下好断点后,我们就可以对程序进行调试了,调试方法:在程序文件中右键,然后点击
debug文件名
按钮。
进入调试状态后,我们现在开始点击
Run to Cursor按钮
,解释器就会继续执行到它应该执行的地方停下来,
我们可以让程序在任意断点处停下来,来观察当前每个变量中的内存值,这样很方便我们排查错误。
下面我们看下调用的函数在循环执行完之后,断点执行到返回值的那一步时的各个变量的内存值,通过调试我们看到
maxdict 中的 max 值和 max 变量值不是同一个值,我们就会想到两个变量不是同一块内存。
到目前为止我们给大家演示了如何使用 Pycharm 自带的调试器对程序下断点,断点调试等功能,当然调试器的功能 远远不止这些,比如我们可以查看函数调用堆栈等等,大家可以在项目开发过程中,熟练使用调试。
大家想想,如果我们的程序在一开始运行时没有出现问题,可能在经过一点时间运行后才出现了莫名其妙的问题, 这个时候我们靠 print 输出和下断点调试是没办法找出问题的。比如我们写的一个服务器程序,跑了半年都没有出现问题, 在半年后的某一天出现了问题,我们不可能用 print 打印半年吧,也不可能人为的下断点调试半年。 此时我们可以使用打日志的方式。
import logging import time logging.basicConfig(filename='mylog.log', format='[%(asctime)s-%(filename)s-%(levelname)s:%(message)s]', level=logging.DEBUG, filemode='a', datefmt='%Y-%m-%d%I:%M:%S %p') def reciprocal(data): for item in data: time.sleep(1) try: print("%f" % (1 / item)) except: logging.error("出现错误,此时除数为:" + str(item)) mylist = [2.0, 9.0, 0.0, 1.0, 5.0] # 假定这写是用户给的做除数的数据 reciprocal(mylist) # 我们求倒数
会使用 Pycharm 自带的调试功能。
养成使用调试技能找程序 bug 的习惯。
写一个类,里面有属性和函数,类外面定义对象,然后调用函数,调试一遍,看看运行运行的过程。
不能代替pdb进行动态调试的工具都不好用