1 异常是什么 —— 程序出错时到底发生了什么
是什么?为什么学?
异常(exception):程序运行时发生的「意外事件」,比如除以 0、读不存在的文件、把 "abc" 转成数字。Python 遇到异常时会立即中断当前代码,抛出一个异常对象一路向上传递;没人接住它,程序就崩溃退出。学会看异常、认识异常,是写任何真实程序的第一步——报错不是敌人,它是在用精确的语言告诉你「哪里出了问题」。程序最怕的不是报错,而是报错后直接崩溃。
底层原理
程序执行时,Python 解释器发现某条语句无法正常执行,就创建一个异常对象(如 ZeroDivisionError("division by zero"))并「抛」出去。异常沿着调用栈(函数层层调用形成的路径)向上传递:中途有 except 接住,程序继续运行;一直没人接,解释器打印 Traceback(回溯信息)并终止程序。异常本质上是一个对象——有类型、有描述、有继承体系(ZeroDivisionError → ArithmeticError → Exception → BaseException,所以捕获 Exception 能接住所有普通异常)。
生活类比
异常像餐厅后厨的传菜铃:厨师发现菜做不了(食材没了),不是把整个餐厅关门,而是按铃把问题「传」出去——服务员(except)听到铃声就来处理。
注意事项总结
1 异常是一个「对象」——把它抓出来看个清楚:
try:
result = 10 / 0
except ZeroDivisionError as e:
print("异常类型:", type(e).__name__)
print("异常内容:", e)
print("是否异常对象:", isinstance(e, Exception))
运行结果:
异常类型: ZeroDivisionError
异常内容: division by zero
是否异常对象: True
10 / 0 触发异常,被 except ZeroDivisionError as e 接住,异常对象存进变量 e。type(e).__name__ 打印类型名;isinstance(e, Exception) 验证它是 Exception 家族的对象。记住:异常不是抽象概念,是看得见摸得着的对象。
2 没人接住异常时,程序长什么样(错误示范,不要运行):
# print(10 / 0)
运行结果(程序终止):
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
看到 Traceback 不要慌,它有三段信息:出错位置(File…line 1)→ 异常类型(ZeroDivisionError)→ 具体原因(division by zero)。读懂这三段,就能定位 90% 的 bug。
易错点
- 错误写法:看到报错就慌,把 Traceback 当「程序废了」的信号 → 问题:错过了报错里最有价值的定位信息 → 正确写法:逐行读 Traceback:位置 → 类型 → 原因。
- 错误写法:用
except:(不写类型)一把抓 → 问题:连KeyboardInterrupt(Ctrl+C)都吞掉,程序无法停止 → 正确写法:写明确类型,或最多用except Exception。
记忆口诀:报错不是坏事,是程序在说话;位置、类型、原因,三行读懂它。
2 try / except —— 把「可能出错」和「出错怎么办」分开
是什么?为什么学?
try 块放「可能出错的代码」,except 块放「出错后怎么办」。这是异常处理的核心机制:让「正常逻辑」和「错误处理」各归其位。让程序遇到错误时不「当场去世」,而是优雅地提示用户并继续运行。
底层原理
try 块里的语句被解释器特殊监控:一旦某条语句抛出异常,块内剩余语句立刻跳过,控制权直接跳到匹配的 except。多个 except 从上到下按顺序匹配——第一个「能接住这个异常」的生效(子类能被父类接住)。except 后写的类型越具体,匹配越精确;越靠上,优先级越高。
生活类比
try/except 像安全气囊 + 应急预案:正常行驶时气囊(正常代码)不干预;一旦撞车(异常发生),气囊瞬间弹出(except 接管),车里的人继续开车。多个 except 像多层安全网:第一层接不住就落到第二层——从上往下试。
注意事项总结
1 try/except 的基本形态——出错后程序继续运行:
try:
num = int("abc")
except ValueError as e:
print("转换失败,原因是:", e)
print("程序继续运行")
运行结果:
转换失败,原因是: invalid literal for int() with base 10: 'abc'
程序继续运行
int("abc") 抛出 ValueError,被 except 接住并打印原因;关键在最后一行——except 处理完后,程序从 try 块之后继续往下执行,没有崩溃。
2 多个 except 按顺序匹配——先写具体的,再写笼统的:
try:
num = int("abc")
except ValueError:
print("命中 ValueError:你输入的不是数字")
except TypeError:
print("命中 TypeError")
运行结果:
命中 ValueError:你输入的不是数字
int("abc") 抛的是 ValueError,Python 从上到下检查:第一个 except ValueError 能接住,立刻生效,后面的不再检查。如果多个 except 都能接住同一个异常,永远是最靠上的生效——所以「具体的」要写在「笼统的」上面。
3 except Exception——兜底网的工作原理(继承链的应用):
try:
num = int("abc")
except Exception as e:
print("捕获到(Exception 能接住 ValueError):", type(e).__name__)
运行结果:
捕获到(Exception 能接住 ValueError): ValueError
ValueError 是 Exception 的子类,所以 except Exception 能接住它,type(e).__name__ 仍能告诉我们真实类型。**except Exception 适合做「最后一层兜底网」**——接住一切普通异常并记录日志。
易错点
- 错误写法:裸写
except:→ 问题:连 Ctrl+C(KeyboardInterrupt)都被吞掉,程序没法停 → 正确写法:写具体类型,或最多except Exception。 - 错误写法:把
except Exception写在最上面 → 问题:后面的具体 except 永远轮不到,白写 → 正确写法:具体的写前面,Exception兜底写最后。
记忆口诀:try 里放险活,except 来接锅;具体在前,兜底在后,顺序决定胜负。
3 else 与 finally —— 「成功之后」与「无论成败」
是什么?为什么学?
完整的异常处理结构有四部分:try(尝试)、except(出错)、else(成功)、finally(无论成败)。else 只在没有异常时执行,把「成功路径」从 try 里单独拎出来;finally 无论有没有异常都执行,是释放资源(关文件、关连接、释放锁)最保险的地方。
底层原理
执行顺序是确定的:① 执行 try 块 → ② 若抛出异常,跳到匹配的 except → ③ 若没有异常,执行 else → ④ 无论②③走哪条路,最后都执行 finally。关键点:else 和 except 互斥(有异常走 except,没异常走 else);finally 是「必经之路」。还有个隐藏细节:**finally 先于 return 执行**——函数里 try 块即使写了 return,也要先跑完 finally 才真正返回。
生活类比
完整的 try 结构像一次出门旅行:try 是「出门逛街」,except 是「路上堵车就改道」,else 是「顺利逛完买到了东西」,finally 是「不管逛没逛成,回家必须锁门」。
注意事项总结
1 完整结构——四条路的执行顺序:
try:
result = 10 / 2
except ZeroDivisionError:
print("除数不能为零")
else:
print("计算成功,结果是", result)
finally:
print("finally:无论成功失败都执行")
运行结果:
计算成功,结果是 5.0
finally:无论成功失败都执行
10 / 2 没出错 → 跳过 except → 执行 else 打印成功结果 → 最后执行 finally。把代码改成 10 / 0 再跑:这次走 except,但 finally 依然执行。两条路都通向 finally。
2 finally 做清理——出错时收尾也照常进行:
try:
with open("missing.txt", "r") as f:
print(f.read())
except FileNotFoundError:
print("文件不存在")
finally:
print("finally:清理资源照常进行")
运行结果:
文件不存在
finally:清理资源照常进行
真实场景:这里换成「数据库连接」,finally 里写「关闭连接」——无论成功失败,连接都被正确释放,不会泄漏资源。
3 补充:finally 先于 return 执行——函数里 try 块即使写了 return,返回值算好后也要先跑完 finally 才真正返回。所以 finally 只做清理,**不要写 return**,否则会覆盖 try 里的返回值。
易错点
- 错误写法:把成功逻辑写在 try 里 → 问题:异常发生后,try 里出错行之后的代码全被跳过,成功逻辑不执行 → 正确写法:成功逻辑放
else,只有「可能出错」的代码放 try。 - 错误写法:在 finally 里写 return → 问题:finally 的 return 会覆盖 try 里的 return,返回值被悄悄改掉 → 正确写法:finally 只做清理,不要写 return。
记忆口诀:try 是尝试,except 是接锅;else 等成功,finally 必锁门。
4 raise 与自定义异常 —— 主动说不,业务自己定义错误
是什么?为什么学?
到目前为止,异常都是程序「被动」触发的。但很多时候业务上不允许的操作需要你主动抛出异常:成绩不能为负数、年龄不能超 200、余额不能为负……raise 就是主动抛异常的语句;自定义异常是继承 Exception 创建自己的异常类,让异常名直接表达业务含义(如 ScoreTooLowError)。
底层原理
raise 异常对象 的本质和解释器抛异常完全一样:创建异常对象并沿调用栈向上传递,等待上层 except 接住。所以 raise 和 except 是一对「生产者 / 消费者」。自定义异常只需 class 名字(Exception): 一行:继承来的构造方法和 __str__ 已能显示错误信息。此外,raise 还有不带参数的用法:在 except 块里把刚接住的异常原样再抛给上层,常用于「先记录日志,再交给上层处理」。
生活类比
raise 像收银台的拒收:顾客拿假币(非法数据)付款,收银员明确说「这张不行」(抛出异常),请顾客换一张。自定义异常则是门店的专属规章:奶茶店说「糖度超出范围」,银行说「余额不足」——错误信息越贴合业务,处理越精准。
注意事项总结
1 raise 主动抛出——把「业务问题」明确抛给调用方:
def set_score(score):
if score < 0:
raise ValueError(f"成绩不能为负数,收到:{score}")
return score
try:
set_score(-5)
except ValueError as e:
print("录入失败:", e)
运行结果:
录入失败: 成绩不能为负数,收到:-5
set_score(-5) 发现成绩为负,raise ValueError(...) 主动抛出一个异常(消息里带上现场数据 -5);调用方用 except ValueError 接住并提示。注意:raise 之后的 return score 不会执行——异常让函数立即中断,控制权交给调用方。
2 自定义异常——让异常名表达业务含义:
class ScoreTooLowError(Exception):
"""成绩过低异常"""
def check_pass(score):
if score < 60:
raise ScoreTooLowError(f"成绩 {score} 不及格")
print("成绩合格")
try:
check_pass(55)
except ScoreTooLowError as e:
print("发现异常:", e)
运行结果:
发现异常: 成绩 55 不及格
class ScoreTooLowError(Exception): 一行定义自定义异常;except ScoreTooLowError 精确接住这一类业务问题。自定义异常让代码读起来像业务说明书。
3 补充:无参数 raise——在 except 块里写 raise(不带参数),把刚接住的同一个异常对象原样抛给上层。真实项目常用「层层上报」:本层先记录日志,再无参数 raise 交给上层处理。
易错点
- 错误写法:
raise ValueError(不带括号) → 问题:抛的是异常类本身而不是异常对象,报错信息显示类而不是描述 → 正确写法:raise ValueError("描述信息")。 - 错误写法:自定义异常类忘了继承
Exception→ 问题:异常变成普通类,except接不住它 → 正确写法:必须写class 名字(Exception):。 - 错误写法:在 except 里想「记录后继续抛」却写了
raise NewError(...)→ 问题:换了新异常,原来的异常信息丢失 → 正确写法:只想原样上抛就用无参数raise。
记忆口诀:raise 主动说不,业务异常自定义;类名继承 Exception,无参 raise 原样抛。
5 assert 断言 —— 快速自我检查
是什么?为什么学?
assert 断言用于检查「这里理应成立的条件」,条件不成立就抛 AssertionError。它适合做程序内部的前置检查,比如函数入参。
注意事项总结
1 基本用法:
def enroll(name, age):
assert age >= 6, "入学年龄必须大于等于 6 岁"
print(f"{name} 报名成功")
enroll("小明", 7) # 正常
# enroll("小强", 4) # 取消注释会抛 AssertionError
运行结果:
小明 报名成功
2 断言与 raise 的区别:raise 处理「用户输入等外部错误」,是程序正式功能的一部分;assert 是开发期「内部假设」的检查,用于快速暴露 bug,通常不用于处理用户输入。
易错点
- 错误写法:用
assert处理用户输入 → 问题:断言是开发期检查,生产环境可能被关闭,处理用户输入要用 try/except + raise → 正确写法:外部输入错误用raise,内部假设用assert。
记忆口诀:assert 查假设,不成立就抛;内部检查用它,用户输入靠 raise。
6 动手实践:健壮版计算器
做一个四则运算计算器,任意输入都不崩溃:除零、输入非数字、运算符不认识,都给出友好提示。
# robust_calculator.py —— 健壮版计算器
class CalcError(Exception):
"""计算器业务异常"""
def calculate(a, b, op):
if op not in ("+", "-", "*", "/"):
raise CalcError(f"不支持的运算符:{op}")
if op == "/" and b == 0:
raise CalcError("除数不能为零")
if op == "+":
return a + b
if op == "-":
return a - b
if op == "*":
return a * b
return a / b
def main():
while True:
expr = input("输入算式(如 3 + 5,quit 退出):")
if expr == "quit":
break
parts = expr.split()
if len(parts) != 3:
print("格式错误,示例:3 + 5")
continue
a_str, op, b_str = parts
try:
a = float(a_str)
b = float(b_str)
result = calculate(a, b, op)
print(f"{a} {op} {b} = {result}")
except ValueError:
print("数字格式不对,请输入合法数字")
except CalcError as e:
print("计算失败:", e)
except Exception as e:
print("未知错误:", e)
if __name__ == "__main__":
main()
运行效果:
输入算式(如 3 + 5,quit 退出):10 / 0
计算失败: 除数不能为零
输入算式(如 3 + 5,quit 退出):abc + 2
数字格式不对,请输入合法数字
输入算式(如 3 + 5,quit 退出):5 % 2
计算失败: 不支持的运算符:%
输入算式(如 3 + 5,quit 退出):3 + 5
3.0 + 5.0 = 8.0
注意程序的「分层」:calculate 负责发现业务问题并抛出自定义异常;main 负责捕获并提示用户。这样职责清晰,后续给计算器加功能也不用改错误处理逻辑。
升级挑战:给计算器加一个「连续运算」功能,把上一次结果存下来,用 ans 表示(比如输入 ans + 10)。
今日总结
异常是对象:有类型、有描述、有继承链(具体异常 → Exception → BaseException)
Traceback 三段信息:出错位置 → 异常类型 → 具体原因,报错不可怕
try / except 把「可能出错」和「出错怎么办」分开,except 后程序继续运行
多个 except 从上到下匹配:具体的写前面,except Exception 兜底写最后
else 只在无异常时执行(成功分支),finally 无论成败都执行(资源清理),且 finally 先于 return
raise 主动抛异常,无参数 raise 把原异常原样上抛(先记录再上报)
自定义异常:class 名字(Exception): 一行定义,让异常名表达业务含义
assert 断言用于开发期内部假设检查,不用于处理用户输入
健壮版计算器面对除零、乱输入、未知运算符都不崩溃
| 报错 | 原因 | 修复 |
|---|---|---|
ZeroDivisionError | 除数为 0 | 运算前判断,或捕获后提示 |
ValueError: invalid literal for int() | 把非数字字符串转成数字 | try 捕获,提示重新输入 |
IndexError | 访问列表不存在的下标(如 split 后取第 4 个元素) | 先检查 len(),再取下标 |
AssertionError | assert 条件不成立 | 检查传入数据,修正逻辑 |
except 没生效,异常继续抛出 | 捕获的类型不对(如代码抛 TypeError 却捕获 ValueError) | 先用 except Exception as e 打印真实类型,再改 |