各位听众,晚上好!我是今晚的讲师,很高兴能和大家一起聊聊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都提供了强大的调试器,例如
PyCharm
、VS Code
等。
选择哪个调试器,取决于你的个人喜好和项目需求。如果你喜欢命令行,ipdb
或pudb
是不错的选择。如果你习惯使用IDE,IDE的调试器可能更适合你。
8. 总结:PDB
,永远的备胎
PDB
就像是编程界的瑞士军刀,虽然不是最锋利的刀,但功能齐全,随时可用。无论你使用什么IDE,无论你喜欢什么调试器,掌握PDB
都是一项非常有价值的技能。
PDB
不仅可以帮助你找到Bug,还可以帮助你更好地理解代码的执行过程,提高你的编程水平。
所以,下次遇到Bug时,不要害怕,勇敢地打开PDB
,让它成为你解决问题的得力助手!
今天的讲座就到这里,谢谢大家!希望大家以后都能和Bug和平共处,写出更健壮、更优雅的代码!