你盯着终端里滚动的几十行“here1”“here2”和“done”却依然不知道程序死在了哪个分支里。print调试法或者说“print大法”是每个Python新手最先学会的魔法哪里出了问题就在哪里放一个print。可当代码规模超过几百行print就会变成一场灾难——你不断添加print又不断注释掉最后甚至分不清哪条输出属于哪次运行。更糟的是print输出的只是你预先想好的东西而程序真正的异常往往在你没想到的地方。print大法的本质是一种盲人摸象你摸到了局部却永远看不到全局。所以我整理了五个Python调试技巧它们不是玄学而是能让你从“瞎猜”升级到“证伪”的实用工具。这五种方法共同指向一个核心理念调试不是找错而是建立对代码的信任。下面我们逐一展开。一、让logging成为程序的“黑匣子”第一个技巧就是把print换成logging模块。你可能会说logging不就是带时间戳的print吗不是的。logging提供了真正的分级日志体系DEBUG、INFO、WARNING、ERROR、CRITICAL。你可以只输出WARNING以上的信息也可以在排查问题时临时把级别调到DEBUG而完全不需要删除或注释任何一行代码。print是向终端喊话logging是给程序装黑匣子。黑匣子不会打扰你但会在关键时刻记录下一切。如何用在代码入口处配置一次import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(name)s: %(message)s ) logger logging.getLogger(__name__)然后在需要观察的地方写logger.info(...)或logger.debug(...)。和print不同的是你可以通过修改配置文件就决定同一套代码在开发时输出多少日志、在生产时输出多少日志。print无法分级logging能让你在需要的时候才拉响警报。想象一下你的程序跑了一个多月突然有一天出了岔子。print的输出早已丢进终端之海而logging的记录还在文件中你甚至可以统计过去30天里每个warning出现的频率——这是print给不了的审计能力。logging的威力还不止于此。你可以为每个模块配置独立的logger比如logger logging.getLogger(myapp.database)再结合logging.config.dictConfig在程序运行时动态地调整某个模块的日志级别。这在微服务或者大型项目中非常实用。print一多就成了噪音logging一多却能变成信号。二、breakpoint()一行代码进入调试器第二个技巧是大名鼎鼎的breakpoint()。在Python 3.7里你只要在可疑位置放一行breakpoint()程序就会停下来进入一个交互式pdb调试器。在这里你可以输入n执行下一步s进入函数内部c继续运行p 变量名打印变量q退出。一次breakpoint()胜过十次print。因为print只会告诉你那一刻的值而调试器让你停在那一刻观察所有状态。举个例子你怀疑calculate(x)返回值不对与其在前后各放一个print不如在调用前写breakpoint()。程序停住后你先看看x是多少再step进入函数看看中间变量怎么变化甚至手动修改某个变量观察接下来的走向。你不再需要猜测哪个分支被执行你可以当场审问它。这种“审问”能力是print永远给不了的因为你问的问题只能在写print时提前想到而调试器允许你临时起意。如果你不希望代码里残留breakpoint()可以把它放进条件里if debug_flag: breakpoint()。然后在环境变量或配置文件中控制开关。这样即使代码上生产也无须删掉调试钩子。再搭配pdb的display(expr)命令可以每次执行到断点时自动显示某个变量。这些细节让调试器不再只是“简化版print”。pdb还有不少高级玩法比如commands、condition但一开始掌握几个基础命令就够了。值得注意的是breakpoint()在Python 3.7之前的pdb语法类似但如果你用Python 3.10还可以在调用时指定调试器breakpoint(PDB_Class)。但别被这些细节吓到一行breakpoint()已经能把你从“print插桩”的泥潭里救出来。三、异常堆栈让错误“死”得明明白白第三个技巧是学会利用异常堆栈和inspect模块。当你的程序抛出异常终端上会刷出traceback。很多人扫一眼最后一行“KeyError: name”就赶紧回代码里加print其实应该完整地读一遍堆栈。print只能告诉你程序死了异常堆栈能告诉你它的死因。堆栈中的每一行都是一个函数调用留下的“犯罪现场”。如果你在except块里还可以主动获取更完整的上下文。比如用traceback.format_exc()把异常信息格式化成字符串记录到日志里用sys.exc_info()拿到异常类、实例和回溯对象。更进一步inspect.currentframe()可以让你查看当前堆栈帧甚至调用者的局部变量。例如在异常处理中import traceback, inspect try: dangerous_code() except Exception: tb traceback.format_exc() logger.error(f炸了:\n{tb}) frame inspect.currentframe() logger.error(f当前局部变量: {frame.f_locals})你能看到的不只是“哪里死了”还有“临死前的现场状态”。真正的调试不是看程序跑了多远而是看程序在哪里迷了路。堆栈和inspect的组合就像在迷宫里留下标记让回溯成为路线图。Python的异常链也是个值得深挖的宝藏。当你写raise new_error from old_error时返回给开发者的信息就包含了完整的因果链。异常堆栈不是错误报告而是一张因果地图。配合logging你可以把异常链和当前上下文一起记录下来等出了事故后再来复盘。这比任何print都可靠因为print不会告诉你“上一个异常是在哪个环节被吞掉的”而异常链会。四、IPython.embed()在代码的任意位置打开交互式救援舱第四个技巧来自IPython的embed()函数。如果说pdb是命令行里的求生刀IPython.embed()就是带着自动补全、彩色高亮和魔法命令的直升机。你只要在代码里写from IPython import embed embed()运行到这一行程序会暂停并展开一个完整的交互式环境你可以访问当前作用域的所有变量用Tab补全甚至用?查看对象文档。print是单向广播交互式调试是双向审讯。在这里你可以立刻验证一个假设改变变量的值运行一段临时代码然后按CtrlD退出程序继续执行。这种“中途插入”的体验远远好于不断print和重启。另一个更实用的场景是“事后调试”。如果你在IPython/Jupyter里运行代码遇到异常后输入%debug它会直接在异常发生处打开交互式调试器所有堆栈帧里的变量都随时可取。你问“刚才的list到底多长”“这个类实例是不是单例”——不用重新跑一遍直接在崩溃现场审问。让你在灾难现场拥有一个私人侦探。对于那些动辄训练几小时的数据管道来说这种能力是救命稻草你不需要从头跑到尾只在出错的位置停下来把真相抠出来。还有一个贴士在脚本的入口处可以设置一个“自动嵌入”的钩子比如使用faulthandler模块或PYTHONBREAKPOINT环境变量。PYTHONBREAKPOINTIPython.embed可以让你每次调用breakpoint()时进入的不是pdb而是IPython。把调试器的选择权交给环境而不是写死在代码里。五、别学“快捷键侠”让IDE调试器替你盯梢第五个技巧是拥抱现代IDE的图形化调试器。VS Code、PyCharm里你只需要在代码行号旁边点一下就能设置一个断点然后按F5运行调试。程序会在断点处停下左侧面板清清楚楚地列出所有局部变量、全局变量和表达式求值。你还能添加“监视”表达式让IDE持续显示某个变量是否越界。调试的最高境界是让工具替你盯梢而你把脑力留给逻辑。IDE调试器最有价值的能力之一是调用堆栈面板。你可以看到当前函数是由谁调用的一层层往上回溯甚至点击任意一层回到当时的变量状态。这种“时空穿梭”对排查一类常见问题特别有效比如某个状态没有被正确修改你可以在赋值语句上设一个条件断点只有当变量等于某个诡异值时停住。print属于勤劳者调试器属于聪明人。当然不是所有环境都适合IDE——服务器上的脚本怎么办这时你可以考虑用pdb的set_trace()或者pdbpp、pudb这些增强工具它们在终端里也能提供近似IDE的体验。如果觉得IDE调试器太“重”可以试试pudb——一个在终端里运行的图形化调试器支持鼠标点击、标签页、堆栈查看。它既保留了终端的轻便又提供了接近于GUI的体验。工具的终极形态是能让你忘记工具本身专注在逻辑上。需要说明的是这五个技巧并不是互相排斥的。logging负责留证据breakpoint负责定点突破traceback负责事后复盘IPython.embed()负责深入探查IDE负责全局可视化。在实际项目中我通常先靠logging定位到可疑模块再写一个breakpoint观察内部状态遇到异常就靠traceback保存现场需要复杂交互时打开IPython.embed()最后在IDE里做整体验证。这套组合拳打下来print的出场率几乎降为零。有人会问完全不用print吗也不是。print用来打印临时性、契约性的输出比如脚本的最终结果并没有错。但把print当成调试工具就像用手电筒寻找宇宙暗物质——不是不行而是太不匹配。print大法是新手村的木剑logging和调试器才是屠龙刀。当你下次想往代码里塞一个print(here)的时候不妨停下来问自己我是想知道程序走到哪了还是想知道程序为什么走到这儿前者是低级的“确认”后者是高级的“归因”。告别print大法不是丢掉一个方法而是换掉一种思考问题的方式。从今天开始试着把第一个print换成logger.info把第二个print换成breakpoint()你会发现自己对代码的控制感正在一点点回来。删除print的过程也是在删除自己曾经的低效习惯。你会发现自己不再害怕报错反而欢迎异常——因为每一次异常都是调试器大显身手的机会。真正的高手不是代码不出错而是出错时能在十分钟内找到那个“why”。这五个技巧就是让你成为那个高手的捷径。当你能从容地设置断点、分析堆栈、修改变量、继续运行你才真正开始掌控代码而不是被代码掌控。