Python高级技术之:`Python`的`PDB`:高级调试技巧,如断点条件、命令别名。

各位听众,晚上好!我是今晚的讲师,很高兴能和大家一起聊聊Python调试界的老朋友,但又常常被忽略的高级技巧——PDB

咱们程序员嘛,谁还没遇到过Bug?调试就像是医生给程序看病,PDB就是我们手中的听诊器、X光机,甚至手术刀。今天,咱们就深入研究一下如何用PDB更精准、更高效地找到并解决问题。

1. PDB入门:不仅仅是breakpoint()

很多人对PDB的初印象可能就是Python 3.7之后引入的内置函数breakpoint()。 这确实是启动PDB最简单的方式。

def my_function(x, y):
    result = x + y
    breakpoint() #程序会在这里停下来,进入PDB调试模式
    final_result = result * 2
    return final_result

my_function(5, 3)

运行这段代码,程序会在breakpoint()处暂停,并进入PDB的交互模式。在这里,你可以像在Python解释器里一样,执行各种命令。

PDB的启动方式远不止这一种。

  • 直接从命令行启动:
python -m pdb your_script.py

这种方式会在脚本的第一行暂停,方便你从一开始就跟踪程序的执行。

  • 程序崩溃后自动进入PDB:

有时候,程序会在运行时崩溃,但你可能错过了崩溃时的关键信息。这时,你可以使用pdb.pm()来事后诸葛亮一把。

import pdb

def buggy_function(x):
    try:
        result = 10 / x
    except ZeroDivisionError:
        pdb.pm()  # 崩溃后自动进入PDB调试模式
    return result

buggy_function(0)

运行这段代码,当ZeroDivisionError发生时,pdb.pm()会让你进入崩溃时的堆栈环境,你可以查看当时的变量值,追溯错误的根源。

2. PDB常用命令:基本功要扎实

进入PDB后,你会看到一个(Pdb)提示符。接下来,你就可以使用各种PDB命令来控制程序的执行、查看变量值。

命令 作用 示例
n 执行下一行代码(next) n
s 进入函数调用(step) s
c 继续执行程序,直到遇到下一个断点或程序结束(continue) c
q 退出PDB(quit) q
p <expr> 打印表达式的值(print) p x, p my_list[0]
pp <expr> 漂亮地打印表达式的值(pretty print) pp my_dict
l 显示当前位置周围的代码(list) l, l 10, 20 (显示10到20行)
b <line> 在指定行号设置断点(break) b 15, b my_function (函数入口设置断点)
cl <bpno> 清除指定编号的断点(clear) cl 1, cl (清除所有断点)
disable <bpno> 禁用指定断点,但不删除 (disable) disable 1
enable <bpno> 启用指定断点 (enable) enable 1
w 显示当前调用堆栈(where) w
a 打印当前函数的参数列表(arguments) a
r 继续执行,直到当前函数返回(return) r
j <line> 跳转到指定行号执行(jump) j 20 (谨慎使用,可能导致程序逻辑混乱)
h <command> 显示指定命令的帮助信息(help) h b, h next
alias 创建或查看别名(alias) alias pb print(, alias (查看所有别名)
unalias 删除别名(unalias) unalias pb

这些命令是PDB的基本功,一定要熟练掌握。只有掌握了这些,才能更好地利用PDB进行高级调试。

3. 断点条件:让调试更精准

仅仅在固定的行号设置断点,有时候效率并不高。例如,你只想在某个变量满足特定条件时才暂停程序,这时就可以使用条件断点。

def process_data(data):
    for i, value in enumerate(data):
        if value > 100:
            breakpoint() # 每次value > 100都会暂停
        print(f"Processing value: {value}")

process_data([10, 50, 120, 80, 150])

上面的代码会在每次value > 100时暂停。但如果数据量很大,或者条件判断比较复杂,每次都手动输入命令查看变量值,效率就太低了。

更好的方式是在设置断点时直接指定条件:

def process_data(data):
    for i, value in enumerate(data):
        print(f"Processing value: {value}")
        pass # 设置断点用的占位符

process_data([10, 50, 120, 80, 150])

运行代码,在PDB中输入:

(Pdb) b 4, value > 100  # 在第4行设置断点,条件是value > 100
(Pdb) c

这样,程序只会当value > 100时才会暂停,大大提高了调试效率。

你还可以使用更复杂的条件,例如:

(Pdb) b 4, i > 2 and value % 2 == 0 # i > 2 并且 value是偶数时暂停

4. 命令别名:定制你的PDB

PDB的命令虽然强大,但有些命令比较长,每次输入都很麻烦。这时,你可以使用alias命令来创建别名,简化你的操作。

例如,你经常需要打印当前行的代码,可以创建一个别名ll(list line):

(Pdb) alias ll l .
(Pdb) ll # 相当于 l .

l . 命令会显示当前行及其周围的代码。

再比如,你经常需要打印某个变量的值,可以创建一个别名pb(print break):

(Pdb) alias pb print
(Pdb) pb x # 相当于 print(x)

更进一步,你可以创建带参数的别名。例如,创建一个别名pv(print variable),可以方便地打印任何变量的值:

(Pdb) alias pv print %1
(Pdb) pv x # 相当于 print(x)
(Pdb) pv my_list[0] # 相当于 print(my_list[0])

%1 表示别名的第一个参数,%2 表示第二个参数,以此类推。

别名还可以包含多个命令,用分号分隔:

(Pdb) alias ps p self; p self.__dict__ # 打印self和self.__dict__

查看所有已定义的别名:

(Pdb) alias

删除别名:

(Pdb) unalias pb

5. .pdbrc.py:让你的别名永久生效

每次启动PDB都要重新定义别名,很麻烦。你可以创建一个.pdbrc.py文件,将别名定义放在这个文件中,每次启动PDB时,PDB会自动加载这个文件。

.pdbrc.py文件应该放在你的用户目录下(例如,C:UsersYourName on Windows, or /home/yourname on Linux/macOS)。

例如,创建一个.pdbrc.py文件,内容如下:

import pdb

class Config(pdb.DefaultConfig):
    def __init__(self):
        super().__init__()
        self.prompt = '(MyPdb) ' # 自定义PDB提示符

pdb.Pdb.prompt = '(MyPdb) ' # 修改默认提示符

def alias_pb(pdb_obj, arg):
    """Print the value of a variable."""
    if arg:
        pdb_obj.p(arg)
    else:
        print("Please provide a variable name.")

pdb.Pdb.alias['pb'] = 'print %1'
pdb.Pdb.alias['ll'] = 'l .'
pdb.Pdb.alias['pv'] = 'print %1'
pdb.Pdb.alias['ps'] = 'p self; p self.__dict__'
pdb.Pdb.alias['c'] = 'continue' # 更短的别名
pdb.Pdb.alias['n'] = 'next'
pdb.Pdb.alias['s'] = 'step'
pdb.Pdb.alias['r'] = 'return'

# 自定义命令
def do_mycmd(self, arg):
    """A custom command."""
    print(f"Executing my custom command with arg: {arg}")

pdb.Pdb.do_mycmd = do_mycmd

# 钩子函数,在每次进入PDB时执行一些操作
def precmd(self, line):
    print(f"About to execute: {line}")
    return line

pdb.Pdb.precmd = precmd

# 钩子函数,在每次退出PDB时执行一些操作
def postcmd(self, stop, line):
    print(f"Finished executing: {line}")
    return stop

pdb.Pdb.postcmd = postcmd

#自定义config
pdb.Pdb.use_rawinput = False #禁用readline库。

重新启动PDB,你会发现别名已经生效了。你还可以自定义PDB的提示符,添加自定义命令,甚至定义钩子函数,在每次进入或退出PDB时执行一些操作。

6. 高级技巧:PDB与多线程、异步编程

PDB在单线程环境下很好用,但在多线程或异步编程环境下,可能会遇到一些问题。

  • 多线程调试:

默认情况下,PDB只会调试当前线程。如果你想调试其他线程,可以使用thread命令。

(Pdb) thread # 列出所有线程
(Pdb) thread <thread_id> # 切换到指定线程
  • 异步编程调试:

异步编程的调试比较复杂,因为代码的执行顺序不确定。PDB本身对异步编程的支持有限,但你可以结合其他工具,例如asyncio.run()asyncio.create_task()来调试异步代码。

import asyncio
import pdb

async def my_coroutine(x):
    print(f"Coroutine started with x: {x}")
    await asyncio.sleep(1)
    pdb.set_trace() # 手动进入PDB
    print(f"Coroutine finished with x: {x}")
    return x * 2

async def main():
    task1 = asyncio.create_task(my_coroutine(5))
    task2 = asyncio.create_task(my_coroutine(10))

    await asyncio.gather(task1, task2)

if __name__ == "__main__":
    asyncio.run(main(), debug=True) #debug=True 可以提供更多信息

在异步函数中使用pdb.set_trace(),可以手动进入PDB,查看当前协程的状态。

7. PDB的替代品:更好的选择?

虽然PDB很强大,但也有一些缺点,例如界面不够友好,命令不够直观。因此,出现了一些PDB的替代品,例如:

  • ipdb: PDB的增强版,使用IPython作为界面,提供更好的代码补全、语法高亮等功能。
  • pudb: 一个全屏的PDB替代品,提供更友好的界面和更强大的功能。
  • IDE调试器: 大多数IDE都提供了强大的调试器,例如PyCharmVS Code等。

选择哪个调试器,取决于你的个人喜好和项目需求。如果你喜欢命令行,ipdbpudb是不错的选择。如果你习惯使用IDE,IDE的调试器可能更适合你。

8. 总结:PDB,永远的备胎

PDB就像是编程界的瑞士军刀,虽然不是最锋利的刀,但功能齐全,随时可用。无论你使用什么IDE,无论你喜欢什么调试器,掌握PDB都是一项非常有价值的技能。

PDB不仅可以帮助你找到Bug,还可以帮助你更好地理解代码的执行过程,提高你的编程水平。

所以,下次遇到Bug时,不要害怕,勇敢地打开PDB,让它成为你解决问题的得力助手!

今天的讲座就到这里,谢谢大家!希望大家以后都能和Bug和平共处,写出更健壮、更优雅的代码!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注