Kodewick
第 21 课,共 31 课目录

错误和异常

第 21 课,共 31 课 · 第 4 部分 · 代码和数据 · 约 20 分钟

有的错误让程序根本跑不起来:少一个引号就是 SyntaxError,Python 一行都不跑。还有的错误要跑到那里才出现。把 "十" 变成数字,写出来没问题,可是做不到,Python 就停在这一行,抛出一个异常:

main.py
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,第二条街一座房子也没有:

main.py
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除以 05 / 0
IndexError列表里没有这个位置[1, 2][5]
KeyError字典里没有这个键{"矿石": 3}["灯油"]
NameError用了一个从没定义过的名字print(lamsp)

try 和 except

把可能出错的代码放进 try 代码块。那里一出异常,Python 不停下来,而是跳到 except 代码块。什么事都没有的话,except 就跳过。

main.py
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 最后一行冒号后面的那句话:

main.py
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 从上往下找,跑第一个对得上的。几种异常想一样处理,就用括号括在一起。

main.py
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")

else 和 finally

后面还能再跟两种代码块。else 只在 try 没出异常、顺利跑完时才跑。finally 每次都跑,不管有没有出错,一定要做的收尾放这里。

main.py
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总是先跑
excepttry 里出了这种异常
elsetry 没出异常
finally总是跑,最后跑

try 里只放可能出错的那一行,其余的放进 else,这样后面万一有意外的错误,不会被误接住。

校验用户输入

input() 返回的永远是字符串,而人什么都可能输(第 6 课)。while True 加上 try,可以一直问,直到答案能用为止。这个例子会依次收到三个回答:

main.py
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 内置的异常一样:

main.py
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 课讲。

main.py
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

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

安全的除法

把 share(ore, miners) 写完:用 // 返回每个矿工分到多少;miners 是 0 时返回字符串 "没有矿工"。用 try 和 except,不用 if。

要显示
6
没有矿工
exercise1.py
1def share(ore, miners):2    return ore // miners3 4print(share(20, 3))5print(share(20, 0))
输出
点「运行」或按 Ctrl+Enter
练习 2

跳过坏读数

把是整数的读数加起来,不是整数的数一数有几个。先显示总数,再显示跳过了几个。

要显示
总数:13
跳过:3
exercise2.py
1readings = ["4", "x", "7", "", "2", "3.5"]2total = 03skipped = 04for r in readings:5    total += int(r)6print(f"总数:{total}")7print(f"跳过:{skipped}")
输出
点「运行」或按 Ctrl+Enter
练习 3

自己抛出异常

让 load_cart(weight) 在重量超过 100 时抛出 ValueError,信息是 太重了:120(信息里是这次的重量)。循环已经会接住它,并显示信息。

要显示
装好了 40
出错:太重了:120
装好了 90
exercise3.py
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}")
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 异常让程序停在出错的那一行;Traceback 列出每一层调用,最外层在上,最后一行说出了什么错。
  • try / except 某种Error: 接住这种异常,程序就能接着跑;as e 拿到错误信息。
  • 好几个 except 分别处理不同的异常;括号把几种异常放在一起处理。
  • else 在没出错时跑;finally 每次都跑。
  • raise ValueError("...") 在自己的代码里抛出异常;class MyError(Exception): pass 造一种自己的异常。