读完本文需要约 1 小时。

文中有较多实践内容,请准备一台连接网络,并装有 Python 的计算机。如果你是新手且时常被周围的新鲜事所吸引,那么请关闭手机网络以及计算机上的社交或新闻软件。认真阅读本文并动手操作,相信你会学到很多。

本文的所有源码在 GitHub 上:https://github.com/xietx1995/pdb_intro

0 前言

一般情况下,开发分为以下三大部分:分析问题(需求)并设计算法;

编码,测试,调试;

发布,持续迭代。

而本文主要关注其中的调试部分。

在很多时候,调试代码都是一件神烦的事情。我们抓破头皮写出的程序,却要花大量的时间来排查其中的错误,难免会让人有点挫败感。所以说掌握一些调试工具和方法,对于我们快速定位错误是很有帮助的。除此之外,当我们学习一些更加高级的和复杂的内容时,我们也可以使用调试工具查看代码内部具体的运行情况,加深我们对知识的理解。

所以不管怎么说,掌握好基本的调试工具和方法都是有益的,不至于在代码出错的时候像个无头苍蝇。特别是对于新手来说,如果遇到错误就发出类似如下的一些对于代码灵魂的拷问:有高手在吗?我的这个代码怎么运行不了?

新手求助!代码运行不了,请问是哪里错了?

...

那么一般情况下,除非是一眼就能看出的错误,是不会有人对发问的人施以援手的。

本文主要向大家详细地介绍 Python 调试工具 pdb。pdb 是 Python 标准库的组成部分,所以我们不需要额外安装。通过 pdb 我们可以查看运行过程中变量的值、设置断点、逐行执行代码、查看代码的调用栈等等。pdb 的另一个好处就是,如果你的环境没有或者不支持 GUI 的话,那么 pdb 能够助你调试代码。

1 运行 pdb

有两种方法使用 pdb,第一种方法是在 Python 代码中插入 pdb.set_trace() 语句,例如:

运行代码:

当 Python 遇到 pdb.set_trace() 语句时,就会进入交互式调试器:

在命令行最前面显示的 (Pdb) 表示你已经进入了 pdb 调试状态,在这个状态下你可以输入命令对代码进行调试。下面的示意图解释了以上三行文字的含义。

第一行表示我们在文件1.1.py中的第 7 行,且处在模块作用域中,后面我们会看到当我们进入函数时,最后一个部分会变为函数名;第二行表示即将要执行的代码;第三行是 pdb 的提示符。

除此之外,我们还可以在不修改代码的前提下,直接按如下方式运行 pdb:

按此种方式运行,调试器会在第一行代码就停住,我们接着就可以输入调试命令进行调试操作。

2 打印表达式的值

在 pdb 中,我们可以使用p命令打印表达式的值。同样以1.1.py为例,我们直接执行该文件,代码会停在函数pdb.set_trace()处:

我们目前所在的位置:在文件1.1.py的第 7 行,且处于模块作用域中;

下一行要执行的代码是z = x + y。

我们可以输入p expression打印表达式 expression 的值:

注意我们在打印z的值时,因为z = x + y还没有执行,所以提示z还未被定义。如最后一行所示,我们也可以像使用 Python 一样在 pdb 中使用表达式。另外我们还可以使用 Python 内置的print函数,但是它和p命令不同,使用print函数必须符合 Python 语法:

p命令支持所有的print函数的用法,例如:

另外,我们还可以使用pp命令来美化输出,pp命令底层调用的是 Python 中的pprint函数。它能够在打印较长的内容(例如很长的列表或字典)时自动美化格式。

3 逐行执行

pdb 有两种命令帮助我们执行代码:n(next):在当前上下文(作用域)中,执行下一行代码,直到程序结束或者返回

s(step):逐行执行代码,如果遇到函数则进入该函数继续逐行执行。

next和step命令的主要区别就是next只在当前作用域中逐行执行,遇到函数时只是正常调用该函数然后移动到下一行,即 step over。而step命令遇到函数调用的时候会进入被调用的函数继续逐行执行,可以理解为 step into。

下面是我们本小节要使用的代码(这份代码有 bug):

我们使用该程序读取并打印文件info.json的内容:

我们直接运行一遍该代码,会出现如下的错误信息:

在函数print_info内,即代码第 13 行报了一个属性错误,提示 None 类型的对象没有 items 属性。你可能以及看出来错误出在哪里了,但是如果是更加复杂的程序,我们可能一眼是看不出来的。接下来我们用 pdb 来调试这段代码。

我们直接运行 pdb:

我们输入 4 次n或者next命令,即来到if语句块:

继续执行一次 n 命令则进入 if 语句块:

此时下一步要执行的语句是 info = load_info('info.json')。如果我们使用 n 命令,那么下一行要执行的语句就是 print_info(info),即我们不会进入函数 load_info:

如果我们使用 s 命令,那么我们会进入 load_info 函数:

注意当 s 命令遇到函数时,pdb 会输出一行 --Call--,表示遇到了一个函数调用,随即进入该函数。同时 pdb 信息的第一个部分的最后也由 module 变为了函数名。

接下来我们可以在该函数内执行 n 或者 s 命令。当执行到函数最后一句时,pdb 会提示函数即将返回:

在执行完最后一句,即将返回时,pdb 会输出一行 --return--。并且在文件位置信息的最后会有返回值信息,这里的函数返回值为 None。注意,此时你可能会疑惑,怎么下一行要执行的代码还指向 info = json.load(f),这里并不是表示又要执行一次这个语句。这一行只是代表函数返回的地方。我们继续输入 n 命令则会结束函数调用,返回到函数调用之后的位置:

即将执行的代码变为了 print_info(info)。此时我们可打印一下 info 变量的值:

发现 info 的值为 None,这就是本小节开头代码运行失败的原因,因为我们向函数 print_info 传入了一个空对象,所以在遍历该对象时,我们在空对象上调用了 items() 方法,触发了 AttributeError。那么我们只需将 load_info 改写如下即可解决这个错误:

再执行一次代码,输出如下:

4 列出源代码

在 pdb 中,我们还可以使用如下两个命令列出程序源代码:l(list) [first[, last]]:列出当前源文件中的代码。如未指定参数,则列出当前行周围的 11 行,或者继续列出未列完的代码。参数为 . 时,列出当前行周围的 11 行;有一个参数,则列出指定行周围的 11 行; 有两个参数则列出指定范围内的行,如果第二个参数小于第一个参数,第二个参数被解释为从第一个参数指定的行向后要列出的行数。当前行使用符号 -> 标记出来。 抛出错误的行如果不是当前行则会被符号 >> 标记出来。

ll(longlist):列出当前所在上下文(作用域)的所有代码。

同样以代码 3.1.py 为例,我们直接运行 pdb:

输入不带参数的 l 命令,列出当前行周围的 11 行:

继续输入不带参数的 l 命令,接续列出 11 行:

输入带参数 . 的 l 命令,列出当前行周围的 11 行:

输入带一个行号参数的 l 命令,列出指定行周围的 11 行:

输入带两个行号参数的 l 命令,列出指定范围内的行:

如果第二个参数比第一个小,例如 l 5,2,则列出第 5 行,然后列出接着的两行:

下面再来看 ll 命令,该命令的作用是例如当前作用域的所有代码。我们在 module 作用域时会列出所有源码:

我们在函数作用域时,会列出所在函数作用域的所有代码:

这两个命令可以帮助我们方便地查看源码,并看清当前的执行上下文。

5 设置断点

pdb 中的断点功能能够帮助我们非常方便地调试代码。使用断点功能,我们能够给指定文件中指定的行或者函数设置断点,并且我们还可以传入一个表达式,只有当满足一定条件时才设置断点。我们使用命令 b 或 break 设置断点,其语法如下:b(break) [([filename:]lineno | function) [, condition]]

参数解释:filename: 要设置断点的源文件,未指定则为当前源文件;

lineno:要设置断点的行数;

function:要设置断点的函数名;

condition:条件表达式,只有当该表达式为 True 时才设置断点。

下面我们来看实例。本小节用到的源文件:

首先,我们在源文件 util.py 中设置一个断点:

如果执行不带参数的 b 命令,则列出所有断点:

我们可输入命令 c 或 cont 或 continue 执行到下一个断点处或者程序结束:

我们也可以使用函数名设置断点:

我们可以使用 disable bpnumber 和enable bpnumber 命令来激活或者关闭(并不删除)断点。bpnumber 是断点的编号。

上面我们首先列出了所有断点,然后关闭了编号为 1 的断点,可以看到 Enb 列对应的值变为了 no,表明该断点已经关闭。接着输入 c 命令,程序没有在断点处终止,而是直接运行直到结束,然后被 pdb 重新载入。我们可以重新激活该断点:

我们可以使用如下命令删除断点:cl(ear) filename:lineno

cl(ear) [bpnumber [bpnumber...]]

例如删除编号为 1 的断点:

或者删除 util.py 中第 3 行的断点:

接着我们来看如何使用条件表达式来设置断点。我们修改一下代码清单 5.1 中的代码:

我们给 print_info 函数传入了 None 作为参数,下面我们来设置一个断点,这个断点只有当函数  print_info 内的 info 参数为 None 时才触发:

我们也可以使用行号来设置断点:

我们可以使用 a 命令查看当前函数参数的值,如果是全局作用域则显示运行脚本时传入的参数:

另外我们还可以使用 tbreak 命令来设置一个临时的断点,临时断点执行一次之后就会被删除。该命令的语法和 b 命令相同。

6 继续执行

目前我们以及掌握了以下几部分内容:打印表达式的值

n 命令和 s 命令

列出源码的 l 和 ll 命令

设置断点,c 命令

使用上面的这些命令,我们已经能够非常方便地调试 Python 程序了。下面我们要介绍的是一个更加方便的命令,unt 或 until 命令:unt(until) [lineno]

用法和参数解释:不带参数:执行到下一行比当前行数大的行,类似 n 命令,但有差别,n 命令是执行逻辑上的下一行,而 unt 是执行代码文件中物理上的下一行。

带参数:执行到比当前行数大的或和当前行数相等的指定行。

两种情况下,unt 命令遇到返回语句都会停下来。

你可以将 unt 命令看作是 c 命令、n 命令还有 b 命令的组合,该命令在应对循环的时候比较有用。例如你在调试如下代码中的 print_and_count_names 函数,但是想跳过循环部分。使用 unt 命令就很方便,我们不需要去手动设置断点:

运行 pdb,进入函数 print_and_count_names:

我们先使用 ll 命令列出了当前作用域的所有代码,然后使用 unt 命令执行到了第 15 行,然后使用 s 命令进入了函数 print_and_count_names。接着我们可以输入两次 unt 命令继续执行两行,然后查看变量 count 的值:

注意上面我只输入了一次 unt,第二次我直接按了回车,所以没有字符显示。直接按回车时,pdb 会执行上次执行的命令。接着我们可以直接使用 unt 命令执行到第 11 行:

当然也可以使用不带行号的 unt 执行:

注意上面的 unt 只在第一次循环是逐行执行的,执行到第 10 行时,下一次 unt 直接执行到了第 11 行,另外 3 次循环在这个过程中已经被执行了。因为 unt 命令只在比当前行大的地方停下来。

7 显示表达式的值

除了 p 和 pp 命令之外,命令 display [expression] 也可以用于显示表达式的值,但是他们的用法有很大的不同。display 命令只有当表达式的值发生改变时才会显示。undisplay [expression] 命令可以取消显示表达式。display [expression]:每次执行到 expression 则显示其值。没有 expression 参数则显示当前作用域内的所有 expression 的值。

undisplay [expression]:不再显示当前作用域内的的某个 expression。没有 expression 参数则清空当前作用域内的所有表达式。

下面是本小节用到的源代码:

我们首先进入函数 print_and_count_names:

然后在第9行设置断点:

然后执行到断点处:

接着使用 display 命令标记我们要显示的表达式:

当我们再次执行到断点时,如果表达式 name 的值发生了改变,那么就会被显示出来:

可以看到,pdb 在 name 改变时,不仅显示了当前的值,还显示了改变之前的值。

我们还可以像下面这样,添加多个自己要查看的值,然后在程序运行的时候,pdb 会为我们显示一组变量值的变化:

我们在第 10 行处设置了断点,并且声明了想要监控的表达式 name 和 count,我们继续执行的时候,pdb 会为我们输出变量值的变化。

8 追踪调用栈

本小节是本篇教程的最后一个部分,我们一起来学习如何使用 pdb 追踪函数调用栈。我们先给出本小节用到的代码(我故意将 8.1.py 中的调用弄得很复杂):

我们在源文件 8.1.py 中调用了源文件 util.py 中的三个函数。现在我们就来看如何使用 pdb 查看整个的调用过程。

首先我们在函数 util.print_info 处设置一个断点,然后执行到该处:

然后我们可以使用命令 w 或者 where 查看当前的调用栈:

调用栈的信息是从下往上看的,这种信息一般称为 trace back,即溯源。由上面的信息可以看到我们当前在函数 print_info 中,同时我们可以看到程序是从哪里进到目前的位置的。下面的示意图代表了上面的调用信息:

除此之外,pdb 有个强大的功能即是在调用栈中自由的移动,以查看各个栈帧(stack frame)中变量的值。我们使用如下两个命令在不同的栈帧之间切换:u(up) [count]:在栈帧中向上移动 count 层,不指定 count 则为 1。

d(down) [count]:在栈帧中向下移动 count 层,不指定 count 则为 1。

例如我们想要将上下文(context)或者说作用域移动到函数 my_func2 中查看变量 info 的值,我们就可以执行:

接着我们试试移动到模块级别的作用域:

再试着反向向下移动到 my_func3 所在的栈帧中:

上面的三次移动如下图所示:

u 和 d 这两个命令很好掌握,只要多玩玩,就能够在源代码中穿梭自如。结合前面七个小节给大家介绍的调试命令,我们可以非常方便地调试 Python 程序。

9 总结

看到这里,相信你已经掌握了 Python 中调试的技巧。我们学会了以下内容:两种运行 pdb 的方式;

显示变量和表达式的值;

逐行执行代码;

列出源代码;

设置断点、激活和关闭断点;

继续执行;

追踪调用栈;

下面是本文中提及到的常用命令:p:打印表达式的值。

pp:同上,但排版更加好看。

n:下一行,相当于 step over。

s:下一行,相当于 step into。

c:执行到下一个断点或程序结束。

unt:执行到下一行或者指定行,两种情况都是执行到比当前行号大的行。

l:列出源码。

ll:列出作用域内全部源码。

b:创建或者列出断点。

w:打印调用栈或者说执行回溯(stack trace)。

u:在调用栈中向上移动。

d:在调用栈中向下移动。

还有几个比较常用的,但是未提及的:h:查看所有可用的命令。

h :某个命令的帮助文档。

h pdb:查看 pdb 的全部文档。

q:退出 pdb

另外更多命令可以参考 pdb 的官方文档。

如果你喜欢带 GUI 界面的调试器,那么可以上网搜索 pycharm debug,spider debug 等相关信息。另外还有一个非常适合查看小段代码运行过程的工具 pythontutor。希望这篇将近 9000 字的教程能够帮到你!

作者:AXin啊阿鑫

公众号:请叫我AXin

分享和转发请注明出处。

python的debug为什么点第一行直接运行结束了_Python 代码调试工具 pdb 快速入门相关推荐

  1. 人人都能学会的python编程教程1:第一行代码

    前言 众所周知,现在IT行业很火,行业薪酬也很高,国家在2017年也发布了人工智能教育的推广计划,人人会编程的时代将要到来.不会编程.不懂编程可能有些跟不上时代的节奏,普通人不懂技术也许会觉得这个很难 ...

  2. python如何读取文件中第一行的元素_python txt读取第一行数据库

    带你读<Python数据分析与数据化运营(第2版)>之一:Python和数据化运营 点击查看第二章点击查看第三章Python数据分析与数据化运营(第2版) 宋天龙 著 第1章 Python ...

  3. 使用python调用minitab_学习Minitab第一天:如何将Minitab链接到Excel以获取快速解答...

    MInitab应该算是比较知名的统计软件了,我现在正在学习当中,然后查找了一些小技巧帮助自己学习,对Minitab感兴趣可以跟着我一起学习啊!想要下载试用的话,可以在在这里下载: Minitab试用 ...

  4. python的ppt库_Python绘图库matplotlib快速入门.ppt

    Python绘图库matplotlib快速入门 *;*;;快速绘图 matplotlib的pyplot子库提供了和matlab类似的绘图API,方便用户快速绘制2D图表.(matplotlib_sim ...

  5. Python csv reader 跳过第一行表头

    在中文互联网上搜出来都是什么奇奇怪怪的,Google 第一条就正解( 太长不看:用 next(csvreader) 参考:Skip the header of a file with Python's ...

  6. Python自带又好用的代码调试工具Pdb学习笔记

    返璞归真 这几天项目有一个linux下部署数据库的操作,数据库使用python进行初始化安装.然后问题来了,由于linux服务器涉及安全要求,除了代码以来的Python3.6版本外不允许安装其他插件与 ...

  7. python速成一小时_Python学习|一小时快速入门python(一)

    1. hello world print('Hello World!')`--># 输出:Hello World! 2.基本数据类型 变量:python的变量直接输入不需要声明 a=5# pri ...

  8. python绘制turtle心电图代码_Python代码详解:入门时间序列分类

    我们接触的大多数时间序列数据主要涉及产生预测的交易.无论是预测产品的需求还是销售额,航空公司的乘客数量或特定股票的收盘价,我们都习惯于利用久经考验的时间序列技术来预测需求. 但随着生成的数据量呈指数增 ...

  9. python区间_Python区间库interval快速入门

    前言 使用python进行数据处理的时候,有时会遇到判断一个数是否在一个区间内的 操作,我们可以使用if else进行判断,但是,既然使用了python,那我们当然是想找一下有没有现成的东西可以用,事 ...

  10. python初学者代码示例_Selenium 快速入门笔记和代码示例(Python版)

    链接 文档链接: 安装 selenium 模块和 Chrome 浏览器驱动 步骤: 安装 Selenium 模块: pip install selenium 下载浏览器驱动(下载即可,无需安装,使用时 ...

最新文章

  1. R语言ggplot2可视化分面直方图(faceting histogram)、使用gghighlight包突出高亮突出每个分面中的一个分组的直方图
  2. C++的Json解析库:jsoncpp和boost
  3. 实战绕过宝塔PHP disable_function 限制getshell
  4. python学习之路 一 :编程语言介绍
  5. python如何制作一个任意列表_在Python中扁平化任意嵌套列表的最快方法是什么?...
  6. UVa 116 (多段图的最短路) Unidirectional TSP
  7. 【转】第8章 前摄器(Proactor):用于为异步事件多路分离和分派处理器的对象行为模式...
  8. 最好免费的 HTML5 JS 网站视频播放器收集
  9. 什么才是真正的项目团队,我来告诉你需要做哪些
  10. C++实现RPG小游戏(彩色版)
  11. 原生小说APP源码,可二次开发,小说阅读,四端互通:android端,ios端,h5端,公众号端
  12. 拔丝芋头的Java学习日记---Day8
  13. 拳王虚拟项目公社:建一个虚拟资源流量池,兼职副业卖虚拟资源商品月入5000
  14. iOS 边学边记 直播原理总结,从理论到实践
  15. linux(ubuntu)查看硬件设备命令
  16. 方案设计阶段的准备工作
  17. 用ch340烧录stm32
  18. 无任何网络提供程序接受指定的网络路径的解决
  19. Oracle 中的一些函数
  20. 下载GHOST重新安装系统,四个硬盘分区变成一个,如何恢复其他硬盘中的资料

热门文章

  1. linux怎么查看hwaddr_怎么查询linux centos mac地址
  2. 创建相册,批量删除,图片预览,上传图片
  3. 【论文笔记】视频分类系列 Temporal Relational Reasoning in Videos (TRN)
  4. php对联广告,对联广告
  5. angular路由守卫
  6. Linux修改时间 修改时区 | Linux时间校准
  7. 2019美研计算机录取,2019美研录取更新 | 春节OFFER大集锦,没有比OFFER更好的新年礼物了!...
  8. 建设银行查看完整卡号
  9. 【学习笔记】斯坦福大学公开课(机器学习) 之生成学习算法:朴素贝叶斯
  10. python colorbar刻度_python-如何添加Matplotlib Colorbar刻度