前面几章,我们已经认识了异常,也学了 try...except。但真正把 Python 学下去,你很快就会发现,写代码只是上半场,排查问题才是下半场。
很多初学者卡住,不是因为不会变量、不会循环、不会函数,而是因为一看到报错信息就发懵。屏幕上一大串英文,看起来密密麻麻,像是在故意为难人。于是有人开始害怕报错,有人开始机械复制粘贴去搜索,还有人一报错就怀疑自己是不是不适合编程。
其实真相恰恰相反。
真正拉开差距的,往往不是谁背的语法更多,而是谁更会看报错、谁更会定位问题、谁更会一步步调试。语法学一阵子都能会,但调试能力才是真正决定你能不能独立写程序的关键。
这一章,我们就专门讲这件事。
一、先接受一个现实:报错不是失败,而是线索
你要先把一个观念彻底扭过来。
报错不是程序在羞辱你。 报错是在给你线索。
程序不会说人话,所以它只能用报错信息告诉你:
哪里出问题了 出了什么类型的问题 问题大概是什么原因
也就是说,报错并不是墙,它更像路标。 只是你刚开始还不会看路标,所以觉得它可怕。
一旦你学会阅读报错信息,你会发现,很多问题并没有想象中那么神秘。甚至有些错误,Python 已经把答案写得非常直接了,只是新手还不会抓重点。
二、为什么说调试能力比背语法更重要
因为真实开发里,没有人能把所有语法细节全部背住。 但每个人都会天天遇到 bug。
你今天可能是列表下标写错。 明天可能是文件路径不对。 后天可能是函数参数类型不符合预期。 再往后,可能是第三方库调用方式不对,或者数据格式和你想的不一样。
这些问题,光靠背书是解决不了的。 真正有用的是一种能力:
看见问题时不慌 能读懂报错 能缩小范围 能验证猜测 能一步步把 bug 挖出来
这就是调试能力。
你可以把它理解成程序员的破案能力。 代码出错,不是让你猜,而是让你查。
三、先看一个最简单的报错长什么样
比如下面这段代码:
print(10 / 0)
运行后,通常会看到类似这样的信息:
Traceback (most recent call last): File "demo.py", line 1, in <module> print(10 / 0)ZeroDivisionError: division by zero
初学者看到这一段,往往会觉得太多了。 其实你现在只需要先学会看三个位置。
第一,看最后一行。 第二,看报错发生在哪一行。 第三,看那一行代码到底在做什么。
只要这三个点抓住,很多问题已经解决一半了。
四、最后一行,通常最重要
还是刚才这个例子:
ZeroDivisionError: division by zero
最后一行通常包含两层关键信息。
第一层,是异常类型。 这里是 ZeroDivisionError
第二层,是简短解释。 这里是 division by zero
翻成人话就是:
发生了除零错误 因为你做了除以 0 的操作
是不是突然就没那么可怕了。
所以以后你看报错,不要一上来就被整屏文字吓住。 先直接把视线拉到最下面。 很多时候,最后一行就是核心结论。
五、出错行数同样非常关键
看这部分:
File "demo.py", line 1, in <module>
这里的重点是 line 1。 意思就是,问题发生在第 1 行。
如果你的程序有几十行、上百行,这个信息特别重要。 因为它能帮你快速缩小范围。
你不需要整份代码从头怀疑到尾。 先去看报错指向的那一行,再结合上下文分析,效率会高很多。
当然,也要注意一个细节:
报错指出的那一行,通常是问题暴露出来的位置,但未必永远是问题真正埋下的源头。 不过对初学者来说,先从那一行入手,已经是最正确的第一步。
六、Traceback 到底是什么
很多人第一次看到这个单词就头疼。
其实你可以把 Traceback 理解成:
程序出事之前,走过的调用路径回放。
尤其是函数嵌套调用、模块调用变多以后,程序可能不是在当前这一行直接出事,而是经过了好几层调用才最终炸掉。
这时候,Traceback 会把调用过程列出来,告诉你:
谁调用了谁 最后在哪一层真正报了错
在入门阶段,你不需要把整段调用栈分析得很深。 先记住一句最实用的话:
看报错时,优先关注最下面的异常类型,再关注和你自己代码有关的那几行。
这样就够用了。
七、新手最容易犯的错:只看英文,不看结构
很多人一看到英文就先怕了,其实报错信息不是让你逐词翻译的,而是让你按结构抓重点的。
你可以按这个顺序看:
先看最后一行是什么错误 再看出错行号 再看具体是哪句代码 最后再想这句代码为什么会这样
这比从第一行开始逐句硬翻有效得多。
调试时,结构感比英语水平重要。 你当然认识更多单词会更轻松,但哪怕英语一般,只要会抓重点,也能处理大部分基础报错。
八、看几个最常见的报错,练一下阅读思路
先看第一个:
age = int('十八')
报错通常类似:
ValueError: invalid literal for int() with base 10: '十八'
这里怎么读?
先看异常类型:ValueError说明值的形式不对
再看后面解释:不能把 '十八' 按十进制整数去转换
翻成人话就是:
int() 想要一个像 18 这样的数字文本 你给了它中文文字 所以失败了
再看第二个:
nums = [1, 2, 3]print(nums[5])
报错通常类似:
IndexError: list index out of range
怎么读?
异常类型:IndexError解释:列表下标超出范围
翻成人话就是:
这个列表没那么长 你访问了不存在的位置
你会发现,一旦你不再死盯英文,而是去理解结构,报错信息其实非常直白。
九、再看一个文件错误
with open('abc.txt', 'r', encoding='utf-8') as f: print(f.read())
如果文件不存在,报错通常类似:
FileNotFoundError: [Errno 2] No such file or directory: 'abc.txt'
怎么读?
异常类型:FileNotFoundError说明文件没找到
后面解释:没有这个文件或目录 而且错误对象已经点名了,就是 abc.txt
这时候你就应该想到去检查:
文件名拼错没 后缀对不对 路径对不对 文件是不是根本不在当前目录
注意,这就是调试的味道了。 报错不是让你害怕,而是在告诉你下一步该检查什么。
十、不要一报错就马上改代码,先判断问题类别
这一步非常重要。
初学者经常一看到报错就开始乱改。这里删一行,那里加一行,最后越改越乱。 更稳妥的做法是,先判断这是哪一类问题。
是类型问题 是索引问题 是键不存在 是文件路径问题 是函数参数问题 还是逻辑边界问题
一旦问题类别判断对了,修复思路会清楚很多。
比如看到 TypeError,你就该优先怀疑类型不匹配。 看到 IndexError,就去看列表长度和下标。 看到 KeyError,就检查字典里到底有没有这个键。 看到 FileNotFoundError,就先查路径和文件名。
也就是说,异常类型其实像分类标签。 它会直接提示你往哪个方向排查。
十一、一个特别实用的原则:从最小问题开始查
比如你写了下面这段代码:
scores = ['90', '85', 'abc', '88']total = 0for item in scores: total += int(item)print(total)
程序报错了。 这时候新手很容易想,我这整段程序是不是都错了。
其实没必要这么大面积怀疑。 更好的想法是:
先看报错类型 大概率是 ValueError那就说明某个 item 不能转成整数 接着去想,列表里哪一项不像数字 很快就能锁定 abc
你看,这就是从最小问题开始查。 不是一上来推翻整份代码,而是先找那个最具体、最可验证的点。
十二、学会怀疑数据,而不只是怀疑代码
这是调试里很重要的一层意识。
很多时候,代码本身没毛病,问题出在数据不符合预期。
比如:
你以为列表里全是数字字符串 结果混进了一个 abc
你以为字典里一定有 age结果某条数据根本没有这个键
你以为用户会输入整数 结果他输入了空字符串
你以为 CSV 的每一行都有三列 结果某一行少了一列
所以调试时,别总盯着语法。 要学会问自己:
我现在拿到的数据,真的是我以为的那个样子吗
这个问题,能帮你少走很多弯路。
十三、print 调试法,初学阶段非常有用
虽然以后你会接触更专业的调试工具,但在入门阶段,print() 依然是非常高效的排查方式。
比如你怀疑是某个数据出了问题,可以这样做:
scores = ['90', '85', 'abc', '88']for item in scores: print('当前处理的数据是:', item) print(int(item))
运行后,一旦程序在 abc 那里报错,你就能从前面的输出看出,问题卡在了哪一项。
这就是最朴素、但非常好用的调试方法。
再比如你不确定变量类型,可以直接打出来:
value = input('请输入内容:')print(value)print(type(value))
很多 bug,并不是代码多难,而是你对当前变量的真实状态判断错了。print() 的作用,就是把隐藏在程序里的状态拉出来给你看。
十四、调试时最该打印什么
新手用 print() 容易走两个极端。
一种是什么都不打印,全靠猜。 另一种是打印一大堆,自己把自己看晕。
更有效的做法是,打印最关键的几类信息:
当前走到哪一步 当前变量的值是什么 当前变量的类型是什么 循环里正在处理哪一项 条件判断有没有进入某个分支
例如:
text = input('请输入年龄:')print('原始输入是:', text)print('输入类型是:', type(text))age = int(text)print('转换后的结果是:', age)
你会发现,很多看不见的问题,一打印就清楚了。
十五、把大问题切成小问题,是调试的核心能力
这点和写函数、拆任务,本质上是一回事。
比如你有一段程序,既要读文件,又要拆分数据,又要做计算,还要输出结果。 一旦报错,你不能把四步混在一起想。
你应该拆开:
是不是文件没读到 是不是数据格式不对 是不是转换类型时失败 是不是计算公式有问题 是不是输出时变量还没准备好
一层一层查,问题就会变小。 问题一小,思路就会清楚。
真正成熟的调试,不是灵光一闪,而是有条理地拆。
十六、一个典型案例:报错行未必是根源,但一定是入口
看这段代码:
info = {'name': '小张'}print(info['age'])
报错会发生在 print(info['age']) 这一行。 这是入口。
但根源是什么?
根源不是 print() 有问题,根源是字典里压根没有 age 这个键。
这就提醒你,调试时不能只盯表面动作。 你要顺着报错往前看:
这个变量是从哪里来的 它本来应该长什么样 为什么会缺少这个值
这也是为什么有些 bug 一开始看起来是在某一行爆炸,真正原因却埋在更早的地方。
但还是那句话,对新手来说,报错行永远是最好的入口。 先从入口进去,再慢慢顺藤摸瓜。
十七、读报错时,一个特别重要的习惯:先自己解释一遍
比如你看到:
TypeError: can only concatenate str not int to str
不要急着复制。 先逼自己用中文解释一遍。
比如你可以说:
这说明我在做字符串拼接时,混进了整数 也就是说,两边类型不一致
当你能把报错翻译成自己的话,问题往往已经理解了一半。 很多人之所以一直不会调试,不是因为报错太难,而是因为他从来没真正试着自己解释过。
十八、不要把 try except 当成掩盖报错的布
这一章虽然讲阅读报错信息,但必须顺手提醒你一句。
有些人一学会 try...except,就喜欢哪里报错哪里包一下。程序表面不报错了,以为问题解决了。其实这很危险。
如果你连错因都没看懂,就急着把异常吞掉,程序只是安静了,不代表正确了。
比如:
try: num = int('abc')except:pass
这段代码不会炸,但也没解决任何问题。 它只是把线索捂住了。
所以在学习阶段,报错首先是拿来读的,不是急着拿来消音的。 先看懂,再决定怎么处理,这个顺序不要反。
十九、调试不是一次猜中,而是一轮轮缩小范围
很多初学者以为高手调试,像算命一样,一眼看出 bug 在哪。 其实大多数时候,不是这样。
更真实的过程通常是:
先看报错 做一个初步猜测 加一点打印 缩小范围 再验证一次 再改代码 再运行观察结果
这其实很像破案。 不是一次锁定,而是不断排除。
所以你也不要要求自己一眼看穿所有问题。 只要你能比刚才更接近真相,就已经在进步。
二十、一个完整的小案例,感受调试思路
看下面这段代码:
scores = ['90', '85', 'abc', '88']total = 0for item in scores: total += int(item)average = total / len(scores)print('平均分是:', average)
程序报错了,怎么办?
第一步,看报错最后一行。 大概率会提示 ValueError
第二步,看出错位置。 问题多半在 int(item) 这一行
第三步,推测原因。 说明 item 里有某个值不能转成整数
第四步,加打印验证:
scores = ['90', '85', 'abc', '88']total = 0for item in scores: print('当前 item 是:', item) total += int(item)average = total / len(scores)print('平均分是:', average)
第五步,重新运行。 你会发现程序在处理到 abc 时出错
第六步,修正逻辑。 比如加异常处理,或者先判断数据是否合法
你看,整个过程并不神秘。 它就是读信息、做假设、验证假设。
这就是调试能力的雏形。
二十一、初学者最容易犯的几个调试误区
第一个误区,一报错先慌。 其实慌没有任何帮助,先读信息才有用。
第二个误区,只看第一行不看最后一行。 很多关键结论都在最后。
第三个误区,只盯代码,不看数据。 现实里很多 bug 都是数据问题。
第四个误区,不做验证,全靠猜。 猜可以有,但一定要用打印、拆分、重跑去验证。
第五个误区,一报错就把整个程序推翻。 其实大部分 bug 只是局部问题,不需要大修大改。
只要你把这些坑慢慢绕开,调试能力就会进步很快。
二十二、给自己建立一个固定排查流程
以后每次程序报错,你都可以按下面这个顺序来:
先看最后一行是什么异常 再看报错出现在第几行 再看这一行代码在做什么 再想这一步依赖的数据是什么 必要时加 print() 看变量值和类型 把问题拆小,再逐步验证
当这个流程重复多了,你会越来越稳。 到后面,很多问题你甚至看一眼就知道该从哪查。
真正有用的能力,往往都是这样练出来的。 不是记住某一句万能口诀,而是在一次次排查中形成手感。
二十三、本章小练习
你可以自己做一个非常有价值的练习。
准备下面四段代码,分别运行,并尝试自己解释每一次报错。
第一段:
print(10 / 0)
第二段:
nums = [1, 2, 3]print(nums[5])
第三段:
info = {'name': '小王'}print(info['age'])
第四段:
value = '12a'print(int(value))
练习时不要急着改代码。 先做三件事:
看最后一行是什么异常 看出错行号 用自己的话解释为什么会出错
这一步训练,非常基础,但非常值钱。 因为它是在练你真正的调试基本功。
二十四、本章总结
这一章最重要的,不是又记住了几个异常名字,而是开始建立调试意识。
报错不是敌人,而是线索。 看报错时,优先看最后一行,再看出错行号。 异常类型会提示你问题属于哪一类。 很多 bug 不只是代码问题,也可能是数据问题。print() 在入门阶段是非常有效的调试工具。 调试的本质,不是瞎猜,而是不断缩小范围、验证猜测。 真正决定你能不能独立写程序的,往往不是会多少语法,而是出了问题以后你能不能自己查下去。
下一章我们继续进入这一阶段的综合收束内容:文件与异常综合实战:做一个小型数据处理器。