一、什么是异常
异常(Exception)就是程序运行的时候撞上的错误。比如:
# 下面这行会直接让程序崩溃
print(10 / 0)
不处理的话,屏幕会刷出一大段红色报错,程序当场停止,后面该做的事全都没做。
异常处理的本意很简单:出了问题别让整个程序死掉,兜住它、告诉用户原因、还能接着往下走。
| 没处理异常 | 处理了异常 |
|---|---|
| 程序崩溃,后面的代码都不跑了 | 只跳过出错的这一段,其余照常运行 |
| 用户看到一堆看不懂的英文报错 | 用户看到一句人话提示,体验更好 |
| 小错拖垮整个程序 | 程序更稳,更适合交给别人用 |
try...except 把它接住。二、先看看不处理会怎样(5 个真实报错)
先看 5 个最常见、不处理就会崩的场面。下面每一段都是真跑出来的报错原文(只看最后一行那句英文就知道错在哪):
① 除以零
print(10 / 0)
ZeroDivisionError: division by zero
② 把字母当数字
int('abc')
ValueError: invalid literal for int() with base 10: 'abc'
③ 打开不存在的文件
open('不存在的文件.txt')
FileNotFoundError: [Errno 2] No such file or directory: '不存在的文件.txt'
④ 字符串和数字相加
print('hello' + 5)
TypeError: can only concatenate str (not "int") to str
⑤ 调不存在的方法
print('abc'.append('x'))
AttributeError: 'str' object has no attribute 'append'
ValueError 表示「值的类型对、但内容不合适」,FileNotFoundError 表示「文件没找到」。学会看这一行,你就已经能看懂大半报错。课程思政|工匠精神与严谨求实 · 报错原文里就有答案
不处理异常时,屏幕会甩出一大段红色文字,但它其实很有礼貌:ValueError、FileNotFoundError 这些类型名加一句说明,已经告诉你错在哪。养成习惯,先自己把最后一行读完,再去问人。能读懂报错的人,解决问题的速度快十倍——这也是遇事先查证、不臆断的严谨做派。
三、最简单的 try...except
try...except 是最核心的写法。把「可能出错的代码」放进 try,把「出错后咋办」放进 except:
try:
print(10 / 0)
except ZeroDivisionError:
print('错误:不能除以零!')
运行结果:
错误:不能除以零!
注意看发生了什么:10 / 0 本来会崩,但被 except ZeroDivisionError: 接住了,于是只打印一句人话提示,程序继续往下走。
| 写成 | 含义 |
|---|---|
try: | 后面缩进的代码,「我担心这里会出错」 |
except ZeroDivisionError: | 如果 try 里抛出的是「除以零」这种错,就执行我下面缩进的代码 |
print('...') | 出错后做的事,必须缩进 |
一步一步照着写
- 写
try:(末尾冒号不能少)。 - 下一行缩进 4 格,写可能出错的代码。
- 另起一行写
except 异常类型:,下面缩进写「兜底」代码。
ZeroDivisionError,如果是别的错(比如把字母当数字),它管不了,程序还是会崩——这是好事,能让你看清到底是哪种错。四、捕获多种异常(多个 except、从具体到通用的顺序)
一段代码可能同时犯好几种错。比如让用户输入两个数相除,用户可能:输字母(ValueError)、输 0(ZeroDivisionError)。用多个 except 分别接住:
try:
a = int(input('请输入被除数 a: '))
b = int(input('请输入除数 b: '))
print('结果:', a / b)
except ValueError:
print('错误:请输入整数!')
except ZeroDivisionError:
print('错误:除数不能为零!')
运行效果(输入 6 和 0):
请输入被除数 a: 请输入除数 b: 错误:除数不能为零!
顺序很重要:具体的在前,通用的在后
Python 会从上往下找第一个能对得上的 except。所以要把具体的异常写在前面,把「兜底的大网」写在最后:
try:
x = float(input('请输入数字: '))
y = float(input('请输入数字: '))
print(x / y)
except ValueError:
print('输入的不是数字')
except ZeroDivisionError:
print('除数不能为零')
except Exception as e:
print('其他错误:', e)
except Exception 写在最前面!它什么错都接,后面的具体 except 就永远轮不到执行,等于白写。顺序错了,提示也会变得很笼统。五、except Exception as e(拿到错误信息)
有时你想「不管什么错,都接住,并把错误信息打印出来」——用 except Exception as e。as e 把异常对象存进变量 e,方便你看详情:
try:
f = open('test.txt', 'r')
except Exception as e:
print('发生了一个错误:', e)
print('错误类型:', type(e).__name__)
运行结果(文件不存在时):
发生了一个错误: [Errno 2] No such file or directory: 'test.txt'
错误类型: FileNotFoundError
两个常用小技巧
| 写法 | 作用 |
|---|---|
str(e) | 异常的说明文字,比如「division by zero」 |
type(e).__name__ | 异常的名字,比如「ZeroDivisionError」 |
试一下:
try:
result = 1 / 0
except Exception as e:
print('异常名字:', type(e).__name__)
print('异常说明:', str(e))
输出:
异常名字: ZeroDivisionError
异常说明: division by zero
except Exception 会把所有错都吞掉,连你代码里的拼写错误也一起藏起来,调试时会非常痛苦。平时写练习可以图省事,正式项目里要尽量写清楚具体是哪种异常。AI 助手小贴士|「什么都捕获」是异常处理第一反模式,让 AI 摆翻车现场。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:为什么不建议直接写 except Exception?给两个「真错误被吞掉」的翻车例子,再讲怎么改成精确捕获
课程思政|诚信与职业操守 · 别把错误藏进地毯下
except Exception 什么错都接,写起来省事,代价却是连自己拼错的变量名也一并被吞掉,调试时找不到北。把错误悄悄咽下去,问题不会消失,只会攒成大坑。工作中也一样:数据对不上、进度有偏差,第一时间说出来,比事后解释来得体面——坦诚是职业操守的底线。
六、else 与 finally(执行顺序对照表)
try 还能配两个搭档:else 和 finally。
| 子句 | 什么时候执行 | 典型用途 |
|---|---|---|
try | 总先执行,放可能出错的代码 | —— |
except | try 里出错时才执行 | 报告错误、补救 |
else | try 里没出错才执行 | 放「正常结果」的处理 |
finally | 不管出没出错都执行 | 关文件、清理资源 |
看一遍完整结构
try:
num = int(input('请输入一个数字: '))
result = 10 / num
print('结果是:', result)
except ValueError:
print('输入的不是有效数字!')
except ZeroDivisionError:
print('不能除以零!')
else:
print('没有异常时执行(else)')
finally:
print('不管怎样,我都会执行')
输入 2 时:
请输入一个数字: 结果是: 5.0
没有异常时执行(else)
不管怎样,我都会执行
输入 abc 时:
请输入一个数字: 输入的不是有效数字!
不管怎样,我都会执行
else 比把代码硬塞进 try 更干净:只有「确实成功了」才做后续处理,不容易误伤。而 finally 像「善后小弟」——无论成败都来收尾,最常用于关掉打开的文件。七、主动抛异常 raise
有些错不是 Python 发现的,是你自己想主动拦下来的。比如年龄不能是负数——用 raise 自己制造一个异常:
def set_age(age):
if age < 0:
raise ValueError('年龄不能为负数!')
print('年龄设置为:', age)
try:
set_age(-5)
except ValueError as e:
print('捕获到错误:', e)
运行结果:
捕获到错误: 年龄不能为负数!
raise 的意思是「这里我认定是错的,直接报错」。常用在函数入口做检查:进来的值不对,立刻拦下,别让错误数据跑进后面的逻辑。
| 场景 | 怎么用 raise |
|---|---|
| 参数不合法 | raise ValueError('说明') |
| 类型不对 | raise TypeError('说明') |
| 自定义的业务错误 | raise 自己的异常类('说明') |
raise 想成「拉响警报」:你发现情况不对,主动按铃,让调用你的人用 except 来接。这样错误原因写在你自己手里,特别清楚。课程思政|数据安全与法治素养 · 脏数据,别放它进门
年龄不能是负数,就在函数入口用 raise ValueError 把它拦下来,不让错误数据流进后面的计算。这道关卡看着麻烦,却是数据质量的第一道防线。入口把好关,后面才省心——将来接入别人的数据源时,先校验、再使用,既不替错误数据背书,也是对自己统计结论负责。
八、自定义异常
当 Python 自带的异常名字不够贴切时,你可以造一个自己的。方法很简单:新建一个类,继承 Exception,里面写 pass 就行:
class PasswordTooShortError(Exception):
pass
def check_password(password):
if len(password) < 6:
raise PasswordTooShortError('密码长度至少6位!')
return '密码合格'
try:
check_password('123')
except PasswordTooShortError as e:
print('密码错误:', e)
运行结果:
密码错误: 密码长度至少6位!
自定义异常让报错一眼就知道是哪一类问题。比如收到 PasswordTooShortError,你立刻知道是密码太短,而不是一个笼统的 ValueError。
class PasswordTooShortError(Exception): 里的 pass 和冒号。空类里至少要有一句 pass,否则会报 IndentationError。Error(如 NegativeNumberError、ConfigLoadError),一看就是异常。九、常见内置异常速查表
下表是新手最常遇到的几种异常。看名字基本就能猜到错在哪:
| 异常类型 | 什么时候出现 | 生活化理解 |
|---|---|---|
ValueError | int('abc')、值不合适 | 类型对,但内容不对 |
TypeError | 'a' + 5、类型不对 | 把两种不搭的东西硬凑一起 |
ZeroDivisionError | 10 / 0 | 除数写了 0 |
IndexError | lst[99] 越界 | 下标超出了列表长度 |
KeyError | d['不存在'] | 字典里没有这个键 |
FileNotFoundError | 打开不存在的文件 | 文件找不着 |
AttributeError | 'abc'.append('x') | 这个对象没有这个方法 |
用法小结
# 抓具体错,对症下药
try:
num = int(user_input)
except ValueError:
print('请输入数字')
except。不确定的时候,先用 except Exception as e: 把错误名字打出来(type(e).__name__),就知道该抓哪一种了。十、十个基础案例(对应 10 道习题)
下面 10 个小案例都来自课堂习题,建议每个都亲手敲一遍。每个案例都给了「示例运行结果」,对照着看。
案例 1:分账时的除零错误
try:
total = int(input('请输入奶茶总价: '))
people = int(input('请输入人数: '))
result = total / people
print('每人分摊:', result)
except ZeroDivisionError:
print('人数不能为 0!')
输入 60 和 0 时输出:
请输入奶茶总价: 请输入人数: 人数不能为 0!
案例 2:处理无效输入(ValueError)
try:
cups = float(input('请点奶茶杯数: '))
print('你点了', cups, '杯')
except ValueError:
print('请输入数字!')
输入 3.5 时输出:
请点奶茶杯数: 你点了 3.5 杯
案例 3:同时处理多种异常
try:
total = int(input('请输入总价: '))
cups = int(input('请输入杯数: '))
print('每杯:', total / cups)
except ValueError:
print('错误:请输入整数!')
except ZeroDivisionError:
print('错误:杯数不能为 0!')
输入 6 和 0 时输出:
请输入总价: 请输入杯数: 错误:杯数不能为 0!
案例 4:通用异常与 finally 块
try:
with open('今日订单.txt', 'r') as f:
print(f.read())
except FileNotFoundError:
print('文件未找到!')
finally:
print('操作完成')
文件不存在时输出:
文件未找到!
操作完成
案例 5:else 子句的使用
try:
total = int(input('请输入总价: '))
cups = int(input('请输入杯数: '))
print('每杯:', total / cups)
except ValueError:
print('错误:请输入整数!')
except ZeroDivisionError:
print('错误:杯数不能为 0!')
else:
print('输入有效')
finally:
print('结束')
输入 10 和 2 时输出:
请输入总价: 请输入杯数: 每杯: 5.0
输入有效
结束
案例 6:主动抛出异常(raise)
def check_cups(cups):
if cups <= 0 or cups > 100:
raise ValueError('杯数不合理')
print('杯数合法')
try:
check_cups(-5)
except ValueError as e:
print('异常信息:', e)
输出:
异常信息: 杯数不合理
案例 7:自定义异常类
class NotEnoughStockError(Exception):
pass
def order_safe(cups):
if cups > 10:
raise NotEnoughStockError('库存不足,最多 10 杯')
return '下单成功'
try:
result = order_safe(20)
except NotEnoughStockError as e:
print('自定义异常:', e)
输出:
自定义异常: 库存不足,最多 10 杯
案例 8:捕获所有异常(谨慎使用)
try:
result = 1 / 0
except Exception as e:
print('发生了异常')
print('异常类型:', type(e).__name__)
print('异常信息:', str(e))
输出:
发生了异常
异常类型: ZeroDivisionError
异常信息: division by zero
案例 9:异常链(raise ... from)
class RecipeLoadError(Exception):
pass
try:
with open('配方.txt', 'r') as f:
data = f.read()
except FileNotFoundError as e:
raise RecipeLoadError('配方文件加载失败') from e
运行结果(最后一行是真正的异常类型):
RecipeLoadError: 配方文件加载失败
真跑时,完整报错里在这行上面还有一段 FileNotFoundError 和一行 The above exception was the direct cause of the following exception——这正是 from e 干的事:把「最初为什么错」钉在新异常下面。平时读报错,仍然从最后一行看起。
案例 10:综合应用——安全分账函数
def split_bill(total, people):
if not isinstance(total, (int, float)) or not isinstance(people, (int, float)):
raise TypeError('参数必须是数字')
if people == 0:
raise ZeroDivisionError('人数不能为零')
return total / people
try:
result = split_bill('60', 2) # 故意传字符串测试
print('结果:', result)
except TypeError as e:
print('类型错误:', e)
except ZeroDivisionError as e:
print('除零错误:', e)
输出:
类型错误: 参数必须是数字
AI 助手小贴士|会语法不等于会决策:什么该捕、用什么捕,练判断。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:出 3 道「该不该捕获、用什么捕获」的决策题:文件打不开、用户输入非数字、除以零,让我先说方案你再审
十一、常见坑
坑 1:空的 except 会藏 bug
try:
num = int(input('请输入数字: '))
except:
pass # 什么都不做,错误被悄悄吞掉
except:(不带任何类型)会接住所有异常,连你拼写变量名写错都一并掩盖,调试时根本找不到问题。
except ValueError:。非要兜底,也至少写成 except Exception as e: 把错误打出来。坑 2:except 的顺序写反了
把通用异常写在前面,后面的具体异常就成了摆设:
try:
x = int('abc')
except Exception:
print('出错了')
except ValueError:
print('这不是数字') # 永远执行不到
坑 3:忘记写冒号和缩进
try
print(1)
except ValueError
print('错')
两处都会报错:try 和 except 末尾都要冒号,下面的代码都要缩进 4 格。
坑 4:在 except 里又抛出新错
try:
print(10 / 0)
except ZeroDivisionError:
print('出错啦' + 1) # 这里又踩了 TypeError
处理异常的代码本身也可能出错。遇到嵌套问题,可以再套一层 try,或者先用 print 把变量看清楚。
try,其余逻辑留在外面,异常才好定位。坑 5:用异常代替正常的 if 判断
# 不推荐:为了判断键存不存在而故意报错
try:
value = d['name']
except KeyError:
value = '默认值'
字典里有没有某个键,直接用 if 'name' in d: 判断更高效、更清楚。异常是用来应对「意外」的,不是用来走正常流程的。
十二、报错手册
下面这些报错都是真跑出来的原文,照抄自本机解释器。哪天屏幕弹出来眼熟的,直接对照处理。
① 除零
print(10 / 0)
ZeroDivisionError: division by zero
原因:除数写成了 0。检查公式或先判断 b != 0 再除。
② 字符串转数字失败
int('abc')
ValueError: invalid literal for int() with base 10: 'abc'
原因:int() 只认数字字符串。用户输入时套一层 try...except ValueError。
③ 文件找不到
open('不存在的文件.txt')
FileNotFoundError: [Errno 2] No such file or directory: '不存在的文件.txt'
原因:路径或文件名写错,文件还没创建。确认文件确实存在,或用 w 模式让程序自己建。
④ 字符串加数字
print('hello' + 5)
TypeError: can only concatenate str (not "int") to str
原因:字符串和数字不能直接相加。要么 str(5),要么用 f-string:print(f'hello{5}')。
⑤ 调了不存在的方法
print('abc'.append('x'))
AttributeError: 'str' object has no attribute 'append'
原因:字符串没有 append 方法(那是列表的)。看清对象类型再调方法。
⑥ 列表下标越界
lst = [1, 2, 3]
print(lst[99])
IndexError: list index out of range
原因:下标超过列表长度。列表只有 3 个元素,下标最大是 2。
⑦ 字典里没有这个键
d = {'name': '小明'}
print(d['age'])
KeyError: 'age'
原因:键 'age' 不存在。用 d.get('age', '默认值') 或先 if 'age' in d: 判断。
⑧ 解包个数对不上
a, b = [1, 2, 3]
ValueError: too many values to unpack (expected 2)
原因:等号左边 2 个变量,右边有 3 个值。变量个数必须和序列长度一致。
⑨ 类型不对
print('年龄:' + 18)
TypeError: can only concatenate str (not "int") to str
原因:和 ④ 一样是「字符串 + 数字」。18 要写成 str(18) 或用 f-string。
⑩ 主动 raise 的自定义错误
raise ValueError('年龄不能为负数!')
ValueError: 年龄不能为负数!
原因:这是你自己在代码里 raise 出来的,不是 Python 的 bug,按提示检查传入的值即可。
AI 助手小贴士|traceback「从下往上看」是规矩,但初学时让 AI 逐行讲更快——讲多了,你自己就会读了。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:这是我遇到的完整 traceback。请从最后一行往上逐行解释含义,指出最根源的错误在哪一行、怎么修,最后教我怎么自己读 traceback:<粘贴完整报错>
十三、随堂自测(10 题)
点击选项即可判分,选错的题会立刻展开解析。做完可以看到正确率和错题清单,也可以重新答题。
1下面哪种写法能防止 10 / 0 让程序崩溃?
- A. 用
if判断一下 - B. 用
try...except接住异常 - C. 用
print()打印 - D. 用
#注释掉
try...except 专门用来接住运行时的异常;if 只能预判已知情况,print 和注释都拦不住崩溃。2int('abc') 会抛出哪种异常?
- A.
IndexError - B.
KeyError - C.
ValueError - D.
TypeError
int() 只能转换像 '123' 这样的数字字符串,'abc' 内容不合适,报 ValueError(值的类型对、但内容不对)。3有多个 except 时,更具体的异常(如 ValueError)应该放在哪里?
- A. 放在最后
- B. 放在最前面
- C. 放在中间随便
- D. 放哪都一样
except。具体异常放前面先接住,通用异常(如 Exception)放最后兜底,顺序反了就白写。4except Exception as e: 中,type(e).__name__ 返回什么?
- A. 异常的类名(字符串,如 'ZeroDivisionError')
- B. 出错的行号
- C. 整个报错堆栈
- D.
None
type(e).__name__ 拿到异常类型的名字(字符串),str(e) 才是那句说明文字。两者配合能看清错在哪。5else 子句在 try 结构里什么时候执行?
- A. 只要发生异常就执行
- B. 程序刚启动时执行
- C. try 里没出错时才执行
- D. 循环结束时执行
else 只在 try 整段都顺利、一个异常都没抛时才执行,常用来放「成功后的正常处理」。6finally 子句什么时候会执行?
- A. 只有没异常时才执行
- B. 不管有没有异常都会执行
- C. 只有出错时才执行
- D. 程序最末尾才执行
finally 是「善后小弟」,无论成败都执行,最适合用来关文件、释放资源这类清理工作。7raise 关键字的作用是?
- A. 捕获异常
- B. 忽略异常
- C. 主动抛出异常
- D. 定义一个函数
raise 让你在发现「值不合法」时主动拉响警报,比如 raise ValueError('年龄不能为负数'),把错误原因攥在自己手里。8自定义一个异常类,应该继承谁?
- A.
Exception - B.
object - C.
BaseException随便选 - D. 不需要继承任何类
Exception(如 class MyError(Exception): pass),这样它才能被普通 except 正常接住。9打开一个不存在的文件,会报什么错?
- A.
ValueError - B.
KeyError - C.
TypeError - D.
FileNotFoundError
FileNotFoundError。确认文件存在,或用 w 模式让它自动创建。10print('hello' + 5) 运行时会怎样?
- A. 正常打印
hello5 - B.
IndexError - C.
KeyError - D.
TypeError
TypeError。要拼接得用 str(5) 或 f-string:print(f'hello{5}')。