0%

为什么要调试代码

说起调试,相信大家都不陌生。如果你不是经常调试代码,说明代码 bug 比较少,或者错误比较直观,再或者就是你并没有体会到调试代码带来的诸多方便和好处。 虽然《代码大全(第 2 版)》提到“调试是确定错误根本原因并纠正此错误的过程。”,但是我认为调试还有帮助编写代码和阅读源码的功能。例如我们在做爬虫开发的时候,一边调试,一边解析网页编写相关代码,写起来可以说非常舒服。通过调试,我们还能准确了解代码的执行顺序,调用的方法等操作,一定程度上可以体会到代码编写者的用心。熟练对代码调试,可以提高我们的工作和学习效率。 我们应该都听说过 bug 一词的由来,第一台大型数字计算机的程序员在一次解决电路故障时,发现一只飞蛾飞到了计算机里面,从那以后计算机故障就被称为 bug。一般来说,调试都用在解决 bug 上,通过单步来跟踪问题位置。就《代码大全》一书而言,调试是在开发中迫不得已才使用的功能,只要保证在编写代码时符合规范,明确需求,调试的难度会降低甚至不需要调试。我认为这只是一种理想状态,毕竟编写的代码没有 bug 还是很难的,各种各样的未知情况都会发生。

代码出现 bug,如何调试?

用来调试的工具有很多,python 标准库内置 pdb,升级版 ipdb。如果使用 pycharm,更是有强大的调试工具。 这次主要介绍 ipdb 的使用,因为 ipdb 属于简单的命令行交互式调试,对于日常开发调试已经够用。如果需要更多功能,建议配合日志模块和使用 pycharm 来实现更多操作。 输入命令来安装 ipdb: python3 -m pip install ipdb 安装好后有两种调试方式,一种是在终端使用 ipdb 后跟需要调试的 py 文件,一种是在代码里设置断点。 先准备好需要调试的 py 文件 test.py。

1
2
3
4
5
6
7
8
9
# test.py

def test():
print("hello, world!")
return 0


if "__main__" == __name__:
test()

先在终端直接使用 ipdb 来调试文件,输入命令: python3 -m ipdb test.py 返回:

1
2
3
4
5
6
> /Users/lijianxun/Documents/Code/test.py(1)<module>()
----> 1 def test():
2 print("hello, world!")
3 return 0

ipdb>

可以看到这样的调用方式是从第一行开始单步执行的。 在代码中设置断点,需要修改 test.py 文件:

1
2
3
4
5
6
7
8
def test():
import ipdb;ipdb.set_trace()
print("hello, world!")
return 0


if "__main__" == __name__:
test()

我们在 print 方法的上一行设置断点,运行此 py 文件将从 print 方法开始单步执行。 python3 test.py 返回:

1
2
3
4
5
6
> /Users/lijianxun/Documents/Code/test.py(3)test()
2 import ipdb;ipdb.set_trace()
----> 3 print("hello, world!")
4 return 0

ipdb>

通过以上两种方式,我们就可以使用命令进行调试。 在交互式命令行中输入 hhelp,将返回可查询到的命令的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ipdb> help

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

Miscellaneous help topics:
==========================
exec pdb

我们只需要 h <命令> 就能查询此命令的相关用法,常用的命令如下。

命令

用法

a

列出当前函数的所有参数

b

设置断点 [b 行号]

c

执行直到断点

cl

清除所有断点。清除时会再次确认,输入 y 即可。

l

打印当前所在位置代码片段

n

执行下一行

p/pp

打印变量 [p 变量名]

q

停止调试

r

继续执行直到当前行数返回

s

进入函数

w

打印当前所在代码位置 [w 行数]

熟练使用上述方法,平时一些调试肯定没问题了。 当然了,调试和测试一样,本身不是改进代码质量的方法,而是诊断代码缺陷并修复的一个过程。

编写代码的时候,如何调试?

像是做爬虫,对网页做解析的时候,我们就可以利用调试这种交互的方式来方便代码的编写。这里举例用到 scrapy shell。 scrapy shell 是一个交互终端,可以在编写爬虫的时候,在未启动 spider 的情况下尝试及调试爬虫代码,用来测试提取的数据是否准确。 终端输入: scrapy shell https://lijianxun.top/ 来启动 scrapy shell。 启动以后我们可以看到 scrapy 提供了许多有用的方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x105659640>
[s] item {}
[s] request <GET https://lijianxun.top/>
[s] response <200 https://lijianxun.top/>
[s] settings <scrapy.settings.Settings object at 0x1056595e0>
[s] spider <DefaultSpider 'default' at 0x105b0f880>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser

这里只举例在交互式命令行下编写代码的好处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [1]: response.xpath("//title/text()")[0].extract()
Out[1]: 'Note. | 简讯的个人博客✨'

In [2]: response.xpath("//header/h2/a/text()")
Out[2]:
[<Selector xpath='//header/h2/a/text()' data='python用5行代码画菱形'>,
<Selector xpath='//header/h2/a/text()' data='基于LNMP搭建wordpress博客'>,
<Selector xpath='//header/h2/a/text()' data='使用 pytest 来测试你的代码'>,
<Selector xpath='//header/h2/a/text()' data='Celery 使用小记'>,
<Selector xpath='//header/h2/a/text()' data='宅宅生活收藏夹——项目心得'>,
<Selector xpath='//header/h2/a/text()' data='docker 镜像下载慢怎么办'>,
<Selector xpath='//header/h2/a/text()' data='使用 Python 抓取知乎美图'>,
<Selector xpath='//header/h2/a/text()' data='使用Flask,Nginx,Gunicorn,Supervisor完成网站部署'>,
<Selector xpath='//header/h2/a/text()' data='使用三种语言实现斐波那契数列'>,
<Selector xpath='//header/h2/a/text()' data='使用Pyqt5 Designer构建桌面应用'>]

这样一边测试提取代码是否正确,一边编写脚本代码,这样下来效率很高。不至于每写好一次提取代码后就跑一次代码。

阅读源码时,如何调试?

这里我们以阅读 Flask 源码为例。先创建一个简单的 Flask 项目(实际项目不建议使用 app.run() 来启动服务)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask


app = Flask(__name__)


@app.route("/")
def test():
return "Hello, Flask."


if __name__ == '__main__':
app.run()

使用 pycharm 编辑器,在 app.run() 一行打上断点,使用 debug 模式运行,按 F7 进入函数,按 F8 单步执行。这样我们就能看出在启动 Flask 服务的时候,调用了哪些函数和方法,变量赋值情况等。 如果你此刻非常想阅读 Flask 源码,推荐从 github 克隆一份代码,使用 git checkout 0.1 命令,切换到第一个版本。因为 Flask 最早的版本只有 flask.py 一个文件,代码一共只有 600 多行,包含了其核心功能,我们可以先从此文件入手,在了解核心代码的基础上,再不断切换 git 分支,学习 Flask 后续版本功能的实现。 其实我们可以发现,调试无处不在。不能只把调试当成诊断 bug 的工具,更应该让调试过程促进我们的工作和学习效率。