【Python系列课程】Python异常处理:try/except让你的程序不再崩溃
📊 阅读时长:18分钟 | 关键词:Python异常处理、try/except、raise、assert、finally、自定义异常
引言:错误是编程的一部分
写代码一定会遇到错误。这不是你水平的问题——即使是 Python 之父 Guido van Rossum 写的代码也会有 Bug。
关键在于:你如何处理错误?
很多初学者采用"鸵鸟策略"——代码报错了,改一下,再跑,再报错,再改。这是最低效的方式。正确的做法是:预见可能的错误,在代码中优雅地处理它们。
Python 提供了完善的异常处理机制,让你可以:
- 捕获并处理运行时错误,而不是让程序崩溃
- 主动抛出异常,让调用方知道出问题了
- 使用 finally 确保资源被正确释放
- 自定义异常类型,让错误信息更有意义
一、错误分类:语法错误 vs 异常
Python 中的错误分为两类:
| 类型 | 发生时机 | 能否处理 | 示例 |
|---|---|---|---|
| 语法错误 | 解析代码时 | ❌ 不能,必须修复代码 | 少了冒号、缩进不对 |
| 异常 | 程序运行时 | ✅ 可以捕获处理 | 除以零、文件不存在 |
# 语法错误:代码根本跑不起来# if True print('hello') # SyntaxError: 少了冒号# 异常:代码语法正确,但运行时出了问题print(1/0)# ZeroDivisionErrorprint(int('abc'))# ValueErroropen('不存在的文件.txt')# FileNotFoundError语法错误必须修复,异常可以处理。本文关注的是后者。
二、try…except:捕获异常
2.1 基本用法
try:# 可能出错的代码result=10/0except:# 出错时执行的代码print('出错了!')2.2 捕获指定类型的异常
裸except会捕获所有异常,但这不推荐——它会吞掉你意料之外的错误。更好的做法是指定异常类型:
defdiv(a,b):try:c=a/bprint(f'{a}/{b}={c}')exceptZeroDivisionError:print('错误:除数为 0!')exceptTypeError:print('错误:类型不匹配!')div(2,1)# 2 / 1 = 2.0div(2,0)# 错误:除数为 0!div('2',2)# 错误:类型不匹配!2.3 捕获多个异常类型
用元组一次捕获多种异常:
defdiv(a,b):try:c=a/bprint(f'{a}/{b}={c}')except(ZeroDivisionError,TypeError):print('发生了除数为 0 或类型错误')div(2,0)# 发生了除数为 0 或类型错误div('2',2)# 发生了除数为 0 或类型错误2.4 获取异常对象:as
defdiv(a,b):try:c=a/bprint(f'{a}/{b}={c}')exceptZeroDivisionErrorase:print(f'除零错误:{e}')print(f'异常类型:{type(e)}')exceptExceptionase:print(f'其他异常:{e}')div(2,0)# 除零错误:division by zerodiv(2,'0')# 其他异常:unsupported operand type(s) for /: 'int' and 'str'2.5 异常的继承层次
Python 的所有异常都继承自BaseException,常用的继承关系如下:
BaseException ├── SystemExit # sys.exit() 触发 ├── KeyboardInterrupt # Ctrl+C 触发 └── Exception # 所有常规异常的基类 ├── ArithmeticError │ ├── ZeroDivisionError │ └── OverflowError ├── TypeError ├── ValueError ├── KeyError ├── IndexError ├── AttributeError ├── FileNotFoundError ├── ImportError └── ...(还有很多)📸[图1:Python 常见异常继承层次结构图]
建议配图:用树状图展示 Python 异常的继承层次。顶层是 BaseException,第二层是 SystemExit、KeyboardInterrupt、Exception。Exception 下展示常用子类:ArithmeticError、TypeError、ValueError、KeyError 等。标注"不要捕获 BaseException"和"推荐捕获具体异常类型"。
重要规则:
- 捕获
Exception可以捕获大部分常规异常 - 不要捕获
BaseException——这会捕获SystemExit和KeyboardInterrupt,导致程序无法正常退出 - 捕获顺序很重要:子类必须写在父类前面
try:# ...exceptZeroDivisionError:# ✓ 子类在前passexceptArithmeticError:# ✓ 父类在后passexceptException:# ✓ 最通用的在最后pass三、try…except…else:没有异常时执行
else子句在try块没有发生任何异常时执行:
defdiv(a,b):try:c=a/bexceptZeroDivisionError:print('除数为 0!')exceptTypeError:print('类型错误!')else:print(f'{a}/{b}={c}')# 只在没有异常时执行div(2,1)# 2 / 1 = 2.0div(2,0)# 除数为 0!为什么需要 else?把print(f'{a} / {b} = {c}')放在try块里也可以,但那样的话,如果print本身出错,会被except误捕获。else让代码意图更明确——“这些代码只在没有异常时执行”。
四、finally:无论是否异常都会执行
finally子句无论如何都会执行——不管有没有异常,不管异常有没有被捕获:
defdiv(a,b):try:c=a/bprint(f'{a}/{b}={c}')exceptZeroDivisionError:print('除数为 0!')else:print('没有异常')finally:print('finally 总是执行')div(2,1)# 2 / 1 = 2.0# 没有异常# finally 总是执行div(2,0)# 除数为 0!# finally 总是执行finally的典型应用场景:释放资源。
# 文件操作:确保文件被关闭f=Nonetry:f=open('data.txt','r')content=f.read()print(content)exceptFileNotFoundError:print('文件不存在')finally:iff:f.close()# 无论如何都会关闭文件print('文件已关闭')📸[图2:try/except/else/finally 执行流程图]
建议配图:用流程图展示完整的异常处理流程——try 块 → 有异常?→ 匹配 except?→ else(无异常时)/ except 块(匹配时)/ 异常向上传播(不匹配时)→ finally(总是执行)。用不同颜色标注各分支路径。
五、raise:主动抛出异常
有时候你需要主动告诉调用方"这里出了问题",用raise:
defdiv(a,b):ifb==0:raiseZeroDivisionError('除数不能为 0!')# 主动抛出异常returna/b# div(2, 0) # ZeroDivisionError: 除数不能为 0!raise的三种形式:
# 形式1:抛出异常实例(最常用)raiseValueError('无效的值')# 形式2:抛出异常类(自动实例化,无自定义消息)raiseValueError# 形式3:在 except 块中重新抛出当前异常try:1/0exceptZeroDivisionError:print('记录日志...')raise# 重新抛出,让上层处理异常链:raise…from
在捕获一个异常后抛出另一个异常时,可以用from保留原始异常信息:
try:num=int(input('请输入数字:'))result=100/numexceptValueErrorase:raiseRuntimeError('输入的不是数字')fromeexceptZeroDivisionErrorase:raiseRuntimeError('不能输入 0')frome六、assert:断言
assert用于调试阶段检查条件,条件为False时抛出AssertionError:
defcalculate_age(birth_year):age=2026-birth_yearassertage>0,f'出生年份{birth_year}不合法!'returnageprint(calculate_age(2000))# 26# print(calculate_age(2030)) # AssertionError: 出生年份 2030 不合法!# assert expression 等价于:# if not expression:# raise AssertionError# assert expression, message 等价于:# if not expression:# raise AssertionError(message)assert vs raise:
| 特性 | assert | raise |
|---|---|---|
| 用途 | 调试、测试、检查"不应发生"的情况 | 处理预期的错误 |
| 可被禁用 | ✅ 用python -O运行时 assert 被跳过 | ❌ 始终生效 |
| 适用场景 | 开发阶段的内部检查 | 生产环境的错误处理 |
重要:不要用assert来做数据验证——因为python -O(优化模式)会跳过所有 assert 语句!
# ❌ 错误:assert 做输入验证defdiv(a,b):assertb!=0,'除数不能为 0'# 优化模式下不执行!returna/b# ✓ 正确:用 raisedefdiv(a,b):ifb==0:raiseValueError('除数不能为 0')returna/b七、自定义异常
当内置异常不够用时,你可以定义自己的异常类型:
classInsufficientFundsError(Exception):"""余额不足异常"""def__init__(self,balance,amount):self.balance=balance self.amount=amountsuper().__init__(f'余额不足!当前余额{balance},尝试取款{amount}')classBankAccount:def__init__(self,balance):self.balance=balancedefwithdraw(self,amount):ifamount>self.balance:raiseInsufficientFundsError(self.balance,amount)self.balance-=amountreturnamount# 使用account=BankAccount(100)try:account.withdraw(200)exceptInsufficientFundsErrorase:print(e)# 余额不足!当前余额 100,尝试取款 200print(f'还差{e.amount-e.balance}元')自定义异常只需要继承Exception(或它的子类),命名通常以Error结尾。
八、常见异常速查表
| 异常类型 | 触发条件 | 示例 |
|---|---|---|
ZeroDivisionError | 除以零 | 1 / 0 |
TypeError | 类型不匹配 | 'a' + 1 |
ValueError | 值不合法 | int('abc') |
IndexError | 索引超出范围 | [1,2][5] |
KeyError | 字典 key 不存在 | {}['a'] |
AttributeError | 属性不存在 | 'abc'.nonexist |
FileNotFoundError | 文件不存在 | open('x.txt') |
ImportError | 导入模块失败 | import nonexist |
NameError | 变量未定义 | print(x) |
StopIteration | 迭代器耗尽 | — |
九、动手练习
练习 1:安全除法函数
defsafe_divide(a,b):"""安全除法,处理各种异常情况"""try:result=a/bexceptZeroDivisionError:return'错误:除数为 0'exceptTypeError:return'错误:类型不支持除法'else:returnf'结果:{result}'print(safe_divide(10,2))# 结果:5.0print(safe_divide(10,0))# 错误:除数为 0print(safe_divide('10',2))# 错误:类型不支持除法练习 2:文件读取安全处理
defread_file_safe(filename):"""安全读取文件,处理各种异常"""try:withopen(filename,'r',encoding='utf-8')asf:returnf.read()exceptFileNotFoundError:returnf'错误:文件{filename}不存在'exceptPermissionError:returnf'错误:没有权限读取{filename}'exceptUnicodeDecodeError:returnf'错误:文件编码不是 UTF-8'print(read_file_safe('不存在的文件.txt'))练习 3:自定义异常
classInvalidAgeError(Exception):"""年龄不合法异常"""passdefvalidate_age(age):ifnotisinstance(age,int):raiseTypeError('年龄必须是整数')ifage<0orage>150:raiseInvalidAgeError(f'年龄{age}不在合法范围内')returnage# 测试try:validate_age(-5)exceptInvalidAgeErrorase:print(e)小结
异常处理是写出健壮 Python 程序的关键:
| 知识点 | 核心内容 |
|:—|:—|:—|
| try/except | 捕获异常,避免程序崩溃 |
| except 指定类型 | 只捕获已知异常,避免隐藏 Bug |
| else | try 无异常时执行 |
| finally | 无论如何都执行(资源释放) |
| raise | 主动抛出异常 |
| assert | 调试断言,不要用于生产环境验证 |
| 自定义异常 | 继承 Exception,让错误更有意义 |
黄金原则:
- 只捕获你能处理的异常
- 不要用裸
except: - 不要在
except块中什么都不做(except: pass) - 异常消息要清晰,帮助调用方理解问题
下一篇文章,我们将进入模块与包——如何组织代码、导入模块、理解__name__ == '__main__'、以及 Python 的模块搜索路径。
本文是「Python从入门到数据分析」系列的第 11 篇,共 24 篇。关注我,不错过后续更新。
