错误和异常
有的错误让程序根本跑不起来:少一个引号就是 SyntaxError,Python 一行都不跑。还有的错误要跑到那里才出现。把 "十" 变成数字,写出来没问题,可是做不到,Python 就停在这一行,抛出一个异常:
1print("开始抄表")2lamps = int("十")3print(f"灯:{lamps}")
开始抄表
Traceback (most recent call last):
File "main.py", line 2, in <module>
lamps = int("十")
ValueError: invalid literal for int() with base 10: '十'发生了什么: 这串字不是一个整数,int() 没法把它变成数。
第 1 行跑了,第 3 行没跑到。异常就是 Python 在说「到这儿我走不下去了」。这一课讲怎么看懂异常,以及怎么接住它,让程序接着跑。
看懂 Traceback
错误发生在函数里面的时候,报错会长一些。下面 report 调用了 cost_per_house,第二条街一座房子也没有:
1def cost_per_house(lamps, houses):2 return lamps * 4 / houses3 4def report(street, lamps, houses):5 cost = cost_per_house(lamps, houses)6 print(street, cost)7 8report("榆树街", 6, 3)9report("橡树巷", 2, 0)
榆树街 8.0
Traceback (most recent call last):
File "main.py", line 9, in <module>
report("橡树巷", 2, 0)
File "main.py", line 5, in report
cost = cost_per_house(lamps, houses)
File "main.py", line 2, in cost_per_house
return lamps * 4 / houses
ZeroDivisionError: division by zero发生了什么: 不能除以 0。
这段报错叫 Traceback(调用回溯)。它把出错时还没结束的每一层调用都列出来,最外层在最上面:第 9 行调用了 report,report 里的第 5 行调用了 cost_per_house,第 2 行出了错。最后一行写着异常的名字和出了什么事。还是从下往上读:先看什么错,再看在哪,最后看程序是怎么走到那里的。
最常碰到的几种异常:
| 异常 | 什么时候出现 | 例子 |
|---|---|---|
ValueError | 类型对,值不对 | int("十") |
TypeError | 类型不能这么用 | "灯" + 1 |
ZeroDivisionError | 除以 0 | 5 / 0 |
IndexError | 列表里没有这个位置 | [1, 2][5] |
KeyError | 字典里没有这个键 | {"矿石": 3}["灯油"] |
NameError | 用了一个从没定义过的名字 | print(lamsp) |
try 和 except
把可能出错的代码放进 try 代码块。那里一出异常,Python 不停下来,而是跳到 except 代码块。什么事都没有的话,except 就跳过。
1for text in ["12", "十", "7"]:2 try:3 lamps = int(text)4 print("拿到", lamps)5 except ValueError:6 print("不是数字:" + text)7print("完成")
拿到 12 不是数字:十 拿到 7 完成
"十" 在 int(text) 那里出错,于是这一次 print("拿到", lamps) 被跳过,循环接着处理 "7"。except 后面写上你预料到的那种异常。except ValueError 只接住这一种;别的异常照样让程序停下,没想到的错误本来就该让你看见。
小技巧: 点单步,看循环第二圈 Python 往哪走:从 int 那一行直接跳到 except。
拿到错误信息
在后面加上 as 和一个名字,就能拿到异常本身。把它显示出来,就是 Traceback 最后一行冒号后面的那句话:
1try:2 lamps = int("十")3except ValueError as e:4 print(f"问题:{e}")5 print(f"种类:{type(e).__name__}")
问题:invalid literal for int() with base 10: '十' 种类:ValueError
记日志、或者告诉用程序的人出了什么错,都用得上。名字 e 只是习惯,叫什么都行。
分别处理几种异常
一个 try 后面可以跟好几个 except,每种异常一个。Python 从上往下找,跑第一个对得上的。几种异常想一样处理,就用括号括在一起。
1stock = {"矿石": 12, "灯油": 0}2 3def per_crate(item, crates):4 try:5 return stock[item] // crates6 except KeyError:7 return "没有这种东西"8 except (ZeroDivisionError, TypeError):9 return "箱数不对"10 11print(per_crate("矿石", 4))12print(per_crate("木头", 4))13print(per_crate("矿石", 0))14print(per_crate("矿石", "四"))
3 没有这种东西 箱数不对 箱数不对
except Exception: 几乎什么异常都接得住。少用:它连变量名拼错这种你想看见的错误也一起藏起来了。
这段会显示什么?
1try:2 print("A")3 x = [1, 2, 3][3]4 print("B")5except KeyError:6 print("C")7except IndexError:8 print("D")9print("E")
先显示 A;三个元素的列表没有位置 3,这是 IndexError。print("B") 被跳过,KeyError 对不上,IndexError 对上了,显示 D。异常已经处理了,程序接着往下,显示 E。
else 和 finally
后面还能再跟两种代码块。else 只在 try 没出异常、顺利跑完时才跑。finally 每次都跑,不管有没有出错,一定要做的收尾放这里。
1def light(text):2 try:3 lamps = int(text)4 except ValueError:5 print(text, "不是数字")6 else:7 print("点亮", lamps, "盏灯")8 finally:9 print("梯子收好了")10 11light("3")12light("三")
点亮 3 盏灯 梯子收好了 三 不是数字 梯子收好了
| 代码块 | 什么时候跑 |
|---|---|
try | 总是先跑 |
except | try 里出了这种异常 |
else | try 没出异常 |
finally | 总是跑,最后跑 |
try 里只放可能出错的那一行,其余的放进 else,这样后面万一有意外的错误,不会被误接住。
校验用户输入
input() 返回的永远是字符串,而人什么都可能输(第 6 课)。while True 加上 try,可以一直问,直到答案能用为止。这个例子会依次收到三个回答:
1while True:2 try:3 lamps = int(input("要点几盏灯?"))4 except ValueError:5 print("请输入一个整数。")6 continue7 if lamps < 0:8 print("不能比 0 还少。")9 continue10 break11print("点亮", lamps, "盏灯")
要点几盏灯?很多 请输入一个整数。 要点几盏灯?-2 不能比 0 还少。 要点几盏灯?5 点亮 5 盏灯
int 挡住不是数字的文字;if 挡住是数字但说不通的值。两道关都过了,break 才离开循环。
raise 和自己的异常
你自己写的函数也能抛出异常。raise 让函数停下,把异常交给调用它的地方,和 Python 内置的异常一样:
1def set_brightness(level):2 if not 0 <= level <= 100:3 raise ValueError(f"亮度要在 0-100 之间,不是 {level}")4 print("亮度", level)5 6set_brightness(80)7try:8 set_brightness(150)9except ValueError as e:10 print(f"拒绝:{e}")11set_brightness(-5)
亮度 80
拒绝:亮度要在 0-100 之间,不是 150
Traceback (most recent call last):
File "main.py", line 11, in <module>
set_brightness(-5)
File "main.py", line 3, in set_brightness
raise ValueError(f"亮度要在 0-100 之间,不是 {level}")
ValueError: 亮度要在 0-100 之间,不是 -5发生了什么: 值不对:类型是对的,但这个值用不了。
怎么处理由调用的地方决定:第二次调用有 try,变成了一句提示;第三次外面没有 try,程序就停了。
你还可以造一种自己的异常,调用的地方就能只接住你这种问题,别的不管。只要两行;class 第 26 课讲,(Exception) 是什么意思第 27 课讲。
1class LampError(Exception):2 pass3 4def change_bulb(lamp, bulbs_left):5 if bulbs_left == 0:6 raise LampError(f"{lamp}没有灯泡可换了")7 return bulbs_left - 18 9bulbs = 110try:11 bulbs = change_bulb("一号灯", bulbs)12 bulbs = change_bulb("二号灯", bulbs)13except LampError as e:14 print(f"停下:{e}")15print(f"剩下的灯泡:{bulbs}")
停下:二号灯没有灯泡可换了 剩下的灯泡:0
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
安全的除法
✓把 share(ore, miners) 写完:用 // 返回每个矿工分到多少;miners 是 0 时返回字符串 "没有矿工"。用 try 和 except,不用 if。
6 没有矿工
1def share(ore, miners):2 return ore // miners3 4print(share(20, 3))5print(share(20, 0))
跳过坏读数
✓把是整数的读数加起来,不是整数的数一数有几个。先显示总数,再显示跳过了几个。
总数:13 跳过:3
1readings = ["4", "x", "7", "", "2", "3.5"]2total = 03skipped = 04for r in readings:5 total += int(r)6print(f"总数:{total}")7print(f"跳过:{skipped}")
自己抛出异常
✓让 load_cart(weight) 在重量超过 100 时抛出 ValueError,信息是 太重了:120(信息里是这次的重量)。循环已经会接住它,并显示信息。
装好了 40 出错:太重了:120 装好了 90
1def load_cart(weight):2 print("装好了", weight)3 4for w in [40, 120, 90]:5 try:6 load_cart(w)7 except ValueError as e:8 print(f"出错:{e}")
这一课学会了
- 异常让程序停在出错的那一行;Traceback 列出每一层调用,最外层在上,最后一行说出了什么错。
try/except 某种Error:接住这种异常,程序就能接着跑;as e拿到错误信息。- 好几个
except分别处理不同的异常;括号把几种异常放在一起处理。 else在没出错时跑;finally每次都跑。raise ValueError("...")在自己的代码里抛出异常;class MyError(Exception): pass造一种自己的异常。