常见报错原因及排查思路
前言
想必在学习python的过程中,最让你感到沮丧和苦恼的是来自运行代码时候的无情报错,那鲜艳的红色预警每次都能让你叹气三连。
但请相信那句亘古不变的鸡汤——“失败乃成功之母”,大佬都是经历了报错的千锤百炼才有了如今的成就,所以不要因此打消你的学习积极性。
那作为一个过来人,为了让你少走一些弯路,这一关我会教授一些如何处理程序错误的小技巧。在此之前,我想和你讲一位女科学家和一只臭虫的故事。
Bug
故事的主人公是被誉为计算机程序之母的格蕾丝·赫伯(Grace Hopper)。时光回到1947年,当时她正在为下图这个庞然大物编制程序。
这是世界上第一部万用计算机的进化版——马克2号(Mark II)。瞧瞧这庞大的机器,可想而知,格蕾丝不止要做脑力活儿,还得做体力活儿。
有一天,她正在调试程序(就跟我们在电脑上运行代码,看终端有没有报错一样),结果老是出现故障。
层层排查后,她拆开了继电器,结果发现有只飞蛾被夹扁在触点中间,从而“卡”住了机器的运行。揪出来之后,格蕾丝幽默地把这只幺蛾子的尸体贴在了她的工作日志上,并喊它叫bug(臭虫)。
从此,bug就化身计算机领域里程序故障的代名词,成为程序员一生如影随形的“亲密敌人”。
Debug
Debug听上去很专业,意思其实就是“捉臭虫”,也就是自查和解决代码中的问题。
你是不是经常遇到过写出来的代码有着莫名其妙的问题?也许是电脑报错无法运行程序,也许是虽然程序可以运行,但效果却和预期中很不一样。
俗话说的好,coding五分钟,debug两小时。所以,这一关我们就来玩玩Debug游戏,看看你能不能帮助他们“杀”死这些捣蛋虫吧!
一起动手来Debug
在python中,常见的bug,一般都是由以下几种原因导致的:
粗心、知识不熟练、思路不清、被动掉坑
相信你学完这篇后,之后遇到bug也能面不改色,更容易发现和解决自己代码中的问题。让我们立马开始吧~
bug 1:粗心
那么首先,我们来看第一种类型:由粗心导致的错误代码:
a = input('请输入密码:')
if a == '123456'
print('通过')
这段代码的意思是:如果用户输入123456,屏幕上会打印出“通过”。但运行这段代码,终端会报错:
请找找这段代码的bug在哪,思考一下如何修改程序后让它可以正常运行。
相信眼尖的你能发现,这段代码的问题是少了一个【英文冒号】。
仔细看报错,其中有3个关键信息。(1)line 2代表这个bug出现在第2行,所以,我们在Debug的时候,可以优先从第2行开始检查。
(2)^代表bug发生的位置,这里指出的位置是第二行末尾。(3)这一行写的是错误类型,SyntaxError指的是语法错误。
一开始可能对错误类型的英文不太熟悉,可以直接复制到百度搜索:
像这样,通过理解报错信息,我们可以快速定位错误的根源。这种阅读、搜索报错信息的能力,在我们以后独立编写愈来愈复杂的程序时显得尤为重要。
我们再来看一段代码。别看这段代码只有两行,却有3处粗心错误,请你帮忙debug,让它能够顺利运行:
for x in range(10):
print(x)
你找到bug了么?
需要特殊说明的是:上面的代码不改缩进不会报错。因为缩进只要结构统一,代码就可以正常运行,但是我们约定俗成是 1 个缩进等于 4 个空格,这不是“语法错误”性质的 bug,而是“不规范”性质的 bug。
这是debug前后的代码,对比一下。
# 这是debug前的代码:
for x in range(10):
print(x)
# 这是debug后的代码:
for x in range(10):
print(x)
虽然粗略地看起来差别不大,但编写代码的严谨性往往就体现在细微之处。
相信以上对你来说应该是小菜一碟,那让我们继续将挑战的难度提升。
下面这段代码的目的是:用户输入用户名和密码,当用户名为abc且密码为123时,显示登录成功,否则登录失败。用户最多可以尝试输入3次。请你修改代码,让程序能顺利运行。
要提醒你,如果光看代码看不出问题,完全可以运行后查看报错信息,再进行修改。
while n<3:
username = input("请输入用户名:")
password = input("请输入密码:")
if username = 'abc' and password = '123':
print("登录成功")
break
else
n=n+1
print("输入有误")
else
print("你输错了三次,登录失败")
怎么样,找到所有问题了吗,代码能正常运行了吗?
这里有3处问题:(1)没有定义变量n,就使用n<3 (2)=是赋值,判断两个值是否相等应该用==(3)2处else后面都漏了冒号。
修改后的参考代码是这样的:
n=0
while n<3:
username = input("请输入用户名:")
password = input("请输入密码:")
if username == 'abc' and password == '123':
print("登录成功")
break
else:
n=n+1
print("输入有误")
else:
print("你输错了三次,登录失败")
运行结果:
请输入用户名:123
请输入密码:gkjhk
输入有误
请输入用户名:abc
请输入密码:123
登录成功
开始学编程的时候,因为粗心导致的bug可能所占的比重最大,这里老师也提供一份自检清单,列出一些新手最容易犯的粗心错误,多有意识地注意以后就能少犯了。
bug 2:知识不熟练
接下来,我们来看看第二种bug:由于知识不够熟练而引起的错误。
让我们一如既往来找茬,该代码的目的是取出列表中的’星期日’,请修改代码让它能正确运行。
week = ['星期一','星期二','星期三','星期四','星期五','星期六','星期日']
sunday = week[7]
print(sunday)
运行结果:
Traceback (most recent call last):
File "/home/python-class/classroom/apps-1-id-5cd9765e19bbcf00015547b8/57/main.py", line 2, in <module>
sunday = week[7]
IndexError: list index out of range
这里的知识错误很明显是:忘记了列表的索引是从0而不是从1开始的。所以,正确的代码应该这样写:
week = ['星期一','星期二','星期三','星期四','星期五','星期六','星期日']
sunday = week[6] # week[6]代表列表“week”中的第7个值
print(sunday)
再来一道题:某同学建了一个空列表a,希望往里面增加3个值,让最后的列表变成 [‘A’,‘B’,‘C’],但写出的代码有误。请你帮忙debug,让它能够顺利运行。
a = []
a = append ('A','B','C')
这里的问题出在append()函数,回顾课堂中append()函数的相关知识,或者搜索“python append”,我们可以知道,并没有a=append(‘A’,‘B’,‘C’) 这种用法。
append()函数是列表的一个方法,要用句点.调用,且append()每次只能接受一个参数,所以正确的写法是这样:
a = []
a.append('A')
a.append('B')
a.append('C')
print(a)
或者写成:
a = []
for i in ('A','B','C'):
a.append(i)
print(a)
这种bug给我们的启示是:当你发现知识点记不清或者不能确定的时候,就要及时复习或者上网搜索。不要强行写出自己不敢确定的代码,这种情况往往容易出错。
如果对某个基础知识点没有熟练的掌握,随着往后知识广度、深度以及项目难度的增加,很可能会增加大量的理解成本,所以多复习、多练习总是没有错滴。
bug 3:思路不清
接下来我们要挑战的是“思路不清bug”,解决了这类bug,对于初学者而言,八成的问题都能自己解决了。
思路不清指的是当我们解决比较复杂的问题时,由于我们对细节和实现手段思考得不够清楚,要么导致一步错,步步错;要么虽然没有报错,但是程序没有达到我们想要的效果。
针对这一点,我先给大家推荐两个工具:
print和#注释
print()
先来看print()函数。这是大家一开始就接触的函数。我们对它的功能也非常熟悉了——打印内容在屏幕上。
但其实它也能成为我们检验对错的里程碑:遇到关键步骤时print出来,看是否达到我们所期望的结果,以此来揪出错误的那一步。
#号注释
#号注释我们也学过,计算机是不会执行代码中的#号和其之后的内容的。
print('伊丽莎白')
#print('我属于我自己')
比如这个就只会执行第一行代码,而第二行代码会被计算机视而不见。
因此,当你写的代码总是不对,又弄不明白哪里不对的时候,使用#号把后面的代码注释掉,一步一步运行,可以帮助排除错误。
print()函数常和#号注释结合在一起用来debug。
练习题1
下面来讲一个例子:
以下是一个同学提交的一段错误代码,大家可以运行看看(记得这里有input()函数,要在终端输入,然后点击enter):
movie = {
'妖猫传':['黄轩','染谷将太'],
'无问西东':['章子怡','王力宏','祖峰'],
'超时空同居':['雷佳音','佟丽娅']
}
name=input('你查询的演员是?')
for i in movie:
actors=[i]
print(actors) #使用print() 函数查看操作是否正确。
if name in actors:
print(name+'出演了'+i)
你可以体验到,这个程序没有达到题目要求的效果,可是又没有报错。这时就需要我们思考,问题出在哪里呢?
1-7行看不出问题,因为字典的写法挺规范的,没出现“粗心bug”。所以,问题应该出现在for循环下面的语句中。
继续看第8行:这位同学想要用for循环遍历这个字典。第9行:这位同学试图取出字典中的值。(对字典用法熟悉的人可以看出,这不符合语法规范)
但如果他自己不知道怎么回事的话,这时,就可以用注释和print()函数来帮助他看看到底是怎么回事,请看下面的第10-12行代码:
movie = {
'妖猫传':['黄轩','染谷将太'],
'无问西东':['章子怡','王力宏','祖峰'],
'超时空同居':['雷佳音','佟丽娅']
}
name=input('你查询的演员是?')
for i in movie:
actors=[i]
print(actors) #使用print() 函数查看操作是否正确。
#if name in actors:
#print(name+'出演了'+i)
我们先把11-12行的代码注释掉,也就是在代码前面分别加一个#。(多行注释有两种快捷操作:1、在需要注释的多行代码块前后加一组三引号’‘’ 2、选中代码后使用快捷键操作:Windows快捷键是ctrl+/,Mac为cmd+/,适用于本地编辑器)
我们先执行前面的代码,并且用print()函数确定这行的操作有无问题。如果运行这段代码,输入’黄轩’,终端会这样显示:
可见这样写取到的全部是字典的键,而非值。这时,就能意识到是这一行出了问题,他可以回看知识点,发现字典的值的取法,然后修改代码。
请你来帮他修改代码吧,回想下要如何取出字典里的值。
参考答案是这样的:
movie = {
'妖猫传':['黄轩','染谷将太'],
'无问西东':['章子怡','王力宏','祖峰'],
'超时空同居':['雷佳音','佟丽娅']
}
name=input('你查询的演员是?')
for i in movie:
actors=movie[i]
if name in actors:
print(name+'出演了'+i)
break
else:
print(name+'的电影暂时没有出演,敬请期待!')
小结
通过这次debug,我们掌握了解决思路不清bug的三步法:
练习题2
打铁要趁热,让我们继续来实操。以下代码是一位学员制作的猜硬币游戏,一共有两次猜的机会。
import random
guess = ''
while guess not in ['正面','反面']:
print('------猜硬币游戏------')
print('猜一猜硬币是正面还是反面?')
guess = input('请输入“正面”或“反面”:')
# 随机抛硬币,0代表正面,1代表反面
toss = random.randint(0,1)
if toss == guess:
print('猜对了!你真棒')
else:
print('没猜对,你还有一次机会。')
guess = input('再输一次“正面”或“反面”:')
if toss == guess:
print('你终于猜对了!')
else:
print('大失败!')
但是,这位学员可能没有想清楚代码的逻辑,导致这个程序有个致命问题:用户永远都不可能猜得对。那要如何把这段代码修改正确呢?请你来试一试。
因为这个程序不报错,所以就算没解决问题,程序也会运行通过。所以需要你自己判断你修改的逻辑是否正确了。
怎么样,你解决了吗?
现在来讲讲解决的思路:
解决问题的第一步,可以先分析代码的含义。
首先,代码开头导入了random模块,并定义了变量 guess。
import random
guess = ''
while guess not in ['正面','反面']:
第三行的guess = input(‘请输入“正面”或“反面”:’)可以分析出,guess是用来存用户输入的变量。
然后继续往下分析while语句:
import random
guess = ''
while guess not in ['正面','反面']:
print('------猜硬币游戏------')
print('猜一猜硬币是正面还是反面?')
guess = input('请输入“正面”或“反面”:')
这一段语句,首先需要弄明白的是while后面跟的条件【guess not in [‘正面’,‘反面’]】的含义。
因为[‘正面’,‘反面’]是一个列表,guess是一个变量,所以看起来这个条件的意思是“如果guess这个变量如果不在[‘正面’,‘反面’]这个列表中,就开始循环”。
所以,你可以先试着随便输入一些东西,看看这个循环会怎么运行:(当我们输入的信息不是【正面】或【反面】的时候,程序会不停地循环,输入【正面】或【反面】的时候可以结束循环)
import random
guess = ''
while guess not in ['正面','反面']:
print('------猜硬币游戏------')
print('猜一猜硬币是正面还是反面?')
guess = input('请输入“正面”或“反面”:')
运行结果:
------猜硬币游戏------
猜一猜硬币是正面还是反面?
请输入“正面”或“反面”:kk
------猜硬币游戏------
猜一猜硬币是正面还是反面?
请输入“正面”或“反面”:ff
------猜硬币游戏------
猜一猜硬币是正面还是反面?
请输入“正面”或“反面”:正面
果然,当我们输入的信息不是【正面】或【反面】的时候,程序会不停地循环。
那到了这里,程序都没有问题。我们接着看。
接下来第11行代码toss = random.randint(0,1),注释上说,这个代码是随机抛硬币,0代表正面,1代表反面。
为了确定random.randint(0,1)功能无误,我们可以写一段代码,随机产生20个数字,看看效果是否如我们所愿。
# 以下代码无需修改,直接运行即可
import random
for i in range(20):
toss = random.randint(0,1)
print(toss)
运行结果:
1
1
1
0
0
0
1
1
0
0
1
0
1
1
0
0
0
1
1
0
运行后我们发现,随机产生的20个数字的确要么是0,要么是1。所以我们继续看代码。
问题应该就出现在后面的条件判断语句了。为了方便发现问题,我们可以加入两个print,把条件判断语句先注释掉,看看guess、toss这两个变量,存起来的是什么东西。(直接运行代码,然后输入【正面】或【反面】)
# 以下代码无需修改,直接运行即可
import random
guess = ''
while guess not in ['正面','反面']:
print('------猜硬币游戏------')
print('猜一猜硬币是正面还是反面?')
guess = input('请输入“正面”或“反面”:')
# 随机抛硬币,0代表正面,1代表反面
toss = random.randint(0,1)
print(guess)
print(toss)
#if toss == guess:
# print('猜对了!你真棒')
#else:
# print('没猜对,再给你一次机会。')
# guess = input('再输一次“正面”或“反面”:')
# if toss == guess:
# print('你终于猜对了!')
#else:
# print('大失败!')
运行结果:
------猜硬币游戏------
猜一猜硬币是正面还是反面?
请输入“正面”或“反面”:正面
正面
0
原来,toss会随机生成0或1,而guess会是“正面”或“反面”,这当然会导致【toss == guess】条件为假!也就是无论怎么猜,条件都不成立。
所以,我们已经找到了代码的bug所在,请你思考一下如何解决这个问题,并修复最终的代码吧!:)
在这里提供两种答案,第一种方法是先创建一个列表:
import random
all = ['正面','反面']
guess = ''
while guess not in all:
print('------猜硬币游戏------')
print('猜一猜硬币是正面还是反面?')
guess = input('请输入“正面”或“反面”:')
toss = all[random.randint(0,1)]
# 随机抛硬币,all[0]取出正面,all[1]取出反面
if toss == guess:
print('猜对了!你真棒')
else:
print('没猜对,再给你一次机会。')
guess = input('再输一次“正面”或“反面”:')
if toss == guess:
print('你终于猜对了!')
else:
print('大失败!')
第二种方法更为取巧,直接把输入的信息限定为’0’或’1’。
import random
guess = ''
while guess not in [0,1]:
print('------猜硬币游戏------')
print('猜一猜硬币是正面还是反面?')
guess = int(input('“正面”请输入0,“反面”请输入1:'))
#注意要用int()将字符串类型转换为数字类型
toss = random.randint(0,1)
if toss == guess:
print('猜对了!你真棒')
else:
print('没猜对,再给你一次机会。')
guess = int(input('再输一次(“正面”请输入0,“反面”请输入1):'))
if toss == guess:
print('你终于猜对了!')
else:
print('大失败!')
相信这个例子能让你感受到如何利用print()和#号注释帮助我们理清解题思路,找到问题所在吧。
那我们来看看最后一种bug——“被动掉坑”。
bug 4:被动掉坑
被动掉坑,是指有时候你的代码逻辑上并没有错,但可能因为用户的错误操作或者是一些“例外情况”而导致程序崩溃。
我们举个例子,当运行以下代码的时候,如果输入的东西不是整数,则程序一定会报错。
age = int(input('你今年几岁了?'))
if age < 18:
print('不可以喝酒噢')
当我们输入的不是整数,程序会这样报错:
这里的“ValueError”的意思是“传入无效的参数”。因为,int()函数只接受数字以及内容为整数的字符串。
当我们输入浮点数的时候,input()函数会返回一个内容为浮点数的字符串,同样不符合int()函数的参数要求。
对于这种“被动掉坑bug”,我们该怎么解决呢?请判断下列思路是否可行:
写个条件判断——用type()函数,判断用户输入的是不是整数(2)写个while循环:如果用户输入的不是整数——让用户重输入。
你可能猜到了,这个思路的确行不通。
为什么这么说呢?你输入一个数字试试就知道了:
#你可以输入1试试。
age = input('你今年几岁了?')
print(age)
print(type(age))
运行结果:
你今年几岁了?1
1
<class 'str'>
可以发现,input()函数默认输出的数据类型是字符串,哪怕你输入的数字1,也会被转化为字符串’1’。所以type()函数并不能帮我们判断输入的到底是不是数字。
到这里,似乎只能强硬提醒用户一定要输入数字了呢?其实不然。
try…except…语句
为了不让一些无关痛痒的小错影响程序的后续执行,Python给我们提供了一种异常处理的机制,可以在异常出现时即时捕获,然后内部消化掉,让程序继续运行。
这就是try…except…语句,具体用法如下:
让我们举个例子。刚才的报错,可以查到报错类型是“ValueError”:
现在你试试不输入整数(比如输入个abc之类的),看代码是否会报错:
try:
age = int(input('请输入一个整数:'))
except ValueError:
print('要输入整数噢')
运行结果:
请输入一个整数:ojo
要输入整数噢
所以,用新学到的知识,来试一试解决之前程序的bug吧:
age = int(input('你今年几岁了?'))
if age < 18:
print('不可以喝酒噢')
参考答案是这样的:
#不用修改代码,直接运行即可,尝试多输入几次非数字
while True:
try:
age = int(input('你今年几岁了?'))
break
except ValueError:
print('你输入的不是数字!')
if age < 18:
print('不可以喝酒噢')
代码要点有两个:(1)因为不知道用户什么时候才会输入正确,所以设置while循环来接受输入,只要用户输入不是数字就会一直循环,输入了数字就break跳出循环。(2)使用try……except……语句,当用户输错的时候会给予提示。
我们再来看一个例子,下列代码的目的是遍历列表中的数字,依次用6除以他们。你可以运行一下,看看报错类型是什么。
num = [1,2,0,3]
for x in num:
print (6/x)
运行结果:
3.0
Traceback (most recent call last):
File "/home/python-class/classroom/apps-1-id-5cd9765e19bbcf00015547b8/2aeac2a0-9762-906f-461b-5e91c156d62f/main.py", line 3, in <module>
print (6/x)
ZeroDivisionError: division by zero
可见,报错类型是ZeroDivisionError,因为小学数学告诉我们,0是不可以做除数的,所以导致后面的循环无法执行。
这时呢,你可以使用try…except语句来帮助你:如果出现ZeroDivisionError就提醒’0不能做除数’,现在请你尝试把代码补全吧~
补全后的参考代码:
num = [1,2,0,3]
for x in num:
try:
#尝试执行下列代码
print (6/x)
#使用6除以num中的元素,并打印
except ZeroDivisionError:
#除非发生ZeroDivisionError报错,执行下列代码:
print('0是不能做除数的!')
#打印“0是不能做除数的!”
运行结果:
6.0
3.0
0是不能做除数的!
2.0
最后,关于Python的所有报错类型,有需要的话可以在这里查阅:https://www.runoob.com/python/python-exceptions.html
总结
好了,我们已经把四种类型的bug和解决方案都介绍了一遍,现在我们稍微回顾一下。
针对粗心造成的bug,有一份自检清单帮助大家检查。
针对知识点不熟造成的bug,要记得多复习,查阅笔记,针对性地做练习掌握用法。
针对思维不清的bug,要多用print()函数和#注释一步步地排查错误。
针对容易被忽略的例外情况从而被动掉坑的bug,可以用try…except语句让程序顺利运行。
最后,希望大家以后在面对bug的时候能耐心地找到问题的根源,尝试独立思考、排查错误,逐步提升debug的能力。