Python 学习笔记(第九期)——异常处理:捕获、抛出与自定义异常
Python 学习笔记(第九期)——异常处理:捕获、抛出与自定义异常
更多Python知识:Python基础入门
我的主页:AOwhisky,这里有更多运维系统性知识整理和其他有趣内容,欢迎与我一起探讨学习~
📖 引言:程序世界的“安全气囊”
在前八期中,我们编写的程序都假设一切“正常”——用户会输入正确的数据、文件一定存在、网络永远畅通。但现实世界远没有那么理想:用户可能输入字母而不是数字,要打开的文件可能被误删,网络请求可能超时……
当这些“意外”发生时,程序会怎样?答案是——程序会崩溃,并抛出一堆普通人看不懂的错误信息。就像一辆没有安全气囊的汽车,一撞就散架。
异常处理就是程序世界的“安全气囊”——它让程序在遇到错误时不会彻底崩溃,而是能够“优雅地”处理问题,要么给出友好的错误提示,要么尝试备选方案。本期我们将系统学习 Python 中的异常处理机制,让程序变得更加健壮和可靠。
📑 本期目录
- 异常概述(什么是异常、常见异常类型)
- 异常捕获语句(
try-except基础) - 捕获多种异常(多
except与元组捕获) else子句与finally子句- 抛出异常(
raise语句) - 断言(
assert语句) - 异常的传递机制
- 自定义异常
- 综合实战演练
- 总结与知识点一览表
正文
一、异常概述
1.1 什么是异常
异常(Exception)是程序在运行过程中发生的错误事件。当 Python 遇到无法继续执行的错误时,会创建一个异常对象并“抛出”(raise)。如果程序中没有处理这个异常的代码,程序会终止执行并显示错误信息(称为“回溯 Traceback”)。
示例:一个典型的异常信息
print(5 / 0)
运行结果:
Traceback (most recent call last):
File "demo.py", line 1, in <module>
print(5 / 0)
~~^~~
ZeroDivisionError: division by zero
异常信息的组成部分:
| 部分 | 说明 | 示例 |
|---|---|---|
| Traceback 头部 | 异常发生的位置(文件名、行号) | File "demo.py", line 1 |
| 错误代码行 | 引发异常的代码 | print(5 / 0) |
| 异常类型 | 异常的具体分类 | ZeroDivisionError |
| 异常描述 | 具体的错误信息 | division by zero |
1.2 常见异常类型
Python 内置了丰富的异常类型,大多数继承自 Exception 类(而 Exception 又继承自 BaseException)。
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── AttributeError
├── FileNotFoundError
├── IndexError
├── KeyError
├── NameError
├── TypeError
└── ValueError
常见异常速查表:
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
ZeroDivisionError |
除数为 0 | 10 / 0 |
NameError |
使用未定义的变量 | print(undefined_var) |
TypeError |
类型不匹配的操作 | "10" + 5 |
ValueError |
值不符合预期 | int("abc") |
IndexError |
索引超出范围 | [1,2,3][5] |
KeyError |
字典键不存在 | {"a":1}["b"] |
AttributeError |
对象没有该属性 | "hello".length |
FileNotFoundError |
文件不存在 | open("notexist.txt") |
💡 白话理解:异常就像看病时的“症状”——不同的症状对应不同的疾病(异常类型),对症下药才能治好病(正确地处理异常)。
二、异常捕获语句——try-except
2.1 基础语法
try:
# 可能出错的代码
pass
except [异常类型 [as 变量名]]:
# 捕获异常后的处理代码
pass
最简单的形式:捕获所有异常(不推荐)。
try:
num = int(input("请输入数字:"))
result = 10 / num
print(result)
except: # 捕获所有异常
print("发生了错误!")
⚠️ 不推荐:捕获所有异常会掩盖具体错误类型,不利于定位问题。应该尽量捕获具体的异常类型。
2.2 捕获具体异常
try:
num = int(input("请输入数字:"))
result = 10 / num
print(result)
except ZeroDivisionError:
print("❌ 除数不能为 0!")
except ValueError:
print("❌ 请输入有效的数字!")
运行示例:
请输入数字:0
❌ 除数不能为 0!
请输入数字:abc
❌ 请输入有效的数字!
请输入数字:5
2.0
2.3 捕获异常并获取异常对象
使用 as 变量名 可以捕获异常对象,获取详细的错误信息。
try:
num = int(input("请输入数字:"))
result = 10 / num
print(result)
except ZeroDivisionError as e:
print(f"❌ 错误类型:{type(e).__name__},信息:{e}")
except ValueError as e:
print(f"❌ 错误类型:{type(e).__name__},信息:{e}")
运行示例:
请输入数字:abc
❌ 错误类型:ValueError,信息:invalid literal for int() with base 10: 'abc'
2.4 多个异常合并捕获
使用元组将多个异常合并到一个 except 中捕获。
try:
num = int(input("请输入数字:"))
result = 10 / num
print(result)
except (ZeroDivisionError, ValueError) as e:
print(f"❌ 输入错误:{e}")
2.5 捕获父类异常
Exception 是所有内置异常的父类,可以捕获所有继承自 Exception 的异常。
try:
num = int(input("请输入数字:"))
result = 10 / num
print(result)
except Exception as e:
print(f"❌ 发生异常:{type(e).__name__} - {e}")
⚠️ 使用 Exception 的注意事项:
- 优点:能捕获所有异常,程序不会崩溃
- 缺点:掩盖了具体异常类型,不利于调试
- 最佳实践:在顶层使用
Exception做“兜底捕获”,下层使用具体异常类型做精细处理
三、else 子句与 finally 子句
3.1 else 子句——没有异常时执行
else 子句中的代码仅在 try 块没有抛出任何异常时执行。
try:
num = int(input("请输入数字:"))
result = 10 / num
except ZeroDivisionError:
print("❌ 除数不能为 0!")
except ValueError:
print("❌ 请输入有效的数字!")
else:
# 只有没有异常时才会执行
print(f"✅ 计算成功!结果是:{result}")
运行示例:
请输入数字:5
✅ 计算成功!结果是:2.0
请输入数字:0
❌ 除数不能为 0!
3.2 finally 子句——无论如何都执行
finally 子句中的代码无论是否发生异常都会执行,通常用于释放资源(如关闭文件、关闭网络连接)。
try:
file = open("data.txt", "r", encoding="utf-8")
content = file.read()
print(content)
except FileNotFoundError:
print("❌ 文件不存在!")
finally:
# 无论是否异常,都要关闭文件
# 注意:如果文件不存在,file 变量未定义,需要先判断
if 'file' in locals():
file.close()
print("✅ 文件已关闭")
try-except-else-finally 完整结构:
try:
# 1. 可能出错的代码
pass
except 异常类型1:
# 2. 捕获特定异常
pass
except 异常类型2:
# 3. 捕获另一种异常
pass
else:
# 4. 没有异常时执行
pass
finally:
# 5. 无论如何都执行(资源清理)
pass
执行流程总结:
| 情况 | try |
except |
else |
finally |
|---|---|---|---|---|
| 无异常 | ✅ 执行 | ❌ 跳过 | ✅ 执行 | ✅ 执行 |
| 有异常且被捕获 | ✅ 执行到异常处 | ✅ 执行 | ❌ 跳过 | ✅ 执行 |
| 有异常且未被捕获 | ✅ 执行到异常处 | ❌ 跳过 | ❌ 跳过 | ✅ 执行(程序随后崩溃) |
四、抛出异常——raise 语句
除了 Python 自动抛出异常,我们也可以使用 raise 语句主动抛出异常。
4.1 基本用法
# 格式1:使用异常类
raise IndexError
# 格式2:使用异常对象(带描述信息)
raise IndexError("索引超出范围")
# 格式3:重新抛出当前异常(在 except 块中使用)
try:
raise ValueError("值错误")
except ValueError:
print("捕获到异常,重新抛出")
raise # 重新抛出,不改变异常类型和消息
示例:输入校验中的主动抛异常
def set_age(age):
if age < 0 or age > 120:
raise ValueError("年龄必须在 0~120 之间")
print(f"年龄设置为:{age}")
try:
set_age(150)
except ValueError as e:
print(f"❌ 设置失败:{e}")
运行结果:
❌ 设置失败:年龄必须在 0~120 之间
4.2 raise...from——异常链
当在捕获一个异常后抛出另一个异常时,可以使用 from 保留原始异常信息。
try:
number = 5 / 0
except ZeroDivisionError as original:
raise ValueError("计算失败") from original
运行结果:
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "demo.py", line 4, in <module>
raise ValueError("计算失败") from original
ValueError: 计算失败
五、断言——assert 语句
assert(断言)用于检查某个条件是否为 True,如果条件为 False,则抛出 AssertionError 异常。
assert 条件表达式 [, 异常信息]
示例:
def divide(a, b):
assert b != 0, "除数不能为 0!"
return a / b
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # AssertionError: 除数不能为 0!
assert vs raise 对比:
| 对比维度 | assert |
raise |
|---|---|---|
| 使用场景 | 调试阶段检查程序内部状态 | 生产环境主动抛出业务异常 |
| 是否可禁用 | ✅ 可通过 -O 命令行参数禁用 |
❌ 无法禁用 |
| 异常类型 | 固定为 AssertionError |
可指定任意异常类型 |
| 适用建议 | 用于检查“不应该发生”的情况 | 用于检查“可能发生”的合法异常 |
💡 生产环境建议:断言主要用于开发和测试阶段,在生产环境中应使用
raise抛出具体的异常类型,而不是依赖断言。
六、异常的传递机制
当函数中发生异常且没有被捕获时,异常会向上传递到调用栈——从当前函数传递到调用它的函数,再到更外层的函数,直到被捕获或程序终止。
func_c() 中发生异常
↓(未被捕获)
func_b() 调用 func_c()
↓(未被捕获)
func_a() 调用 func_b()
↓(未被捕获)
main() 调用 func_a()
↓(未被捕获)
程序崩溃并输出 Traceback
示例:异常的传递:
def func_c():
print("func_c 开始")
raise ValueError("func_c 中的异常")
print("func_c 结束") # 不会执行
def func_b():
print("func_b 开始")
func_c()
print("func_b 结束") # 不会执行
def func_a():
print("func_a 开始")
try:
func_b()
except ValueError as e:
print(f"✅ 在 func_a 中捕获异常:{e}")
print("func_a 结束")
func_a()
运行结果:
func_a 开始
func_b 开始
func_c 开始
✅ 在 func_a 中捕获异常:func_c 中的异常
func_a 结束
传递路径:
func_c 抛出 ValueError
↓ 未被捕获,向上传递
func_b 未捕获
↓ 未被捕获,向上传递
func_a 中的 try 捕获了 ValueError
✅ 异常被成功处理
💡 白话理解:异常传递就像“逐级上报”——下属解决不了的问题,上报给主管;主管解决不了,上报给经理;经理解决了,问题结束。如果一路上都没人能处理,那整个公司就“宕机”了。
七、自定义异常
当 Python 内置的异常类型不足以描述我们的业务错误时,可以创建自定义异常类。
创建方法:继承 Exception 类或其子类,类名通常以 Error 结尾。
class ShortPasswordError(Exception):
"""密码长度不足异常"""
def __init__(self, length, min_length):
self.length = length
self.min_length = min_length
super().__init__(f"密码长度为 {length},至少需要 {min_length} 位")
def set_password(password):
min_len = 6
if len(password) < min_len:
raise ShortPasswordError(len(password), min_len)
print(f"✅ 密码设置成功:{password}")
try:
set_password("123")
except ShortPasswordError as e:
print(f"❌ {e}")
运行结果:
❌ 密码长度为 3,至少需要 6 位
自定义异常的最佳实践:
| 实践 | 说明 |
|---|---|
| 命名规范 | 类名以 Error 结尾,如 ValidationError |
| 继承体系 | 创建一个基类异常(如 AppError),所有自定义异常继承它 |
| 信息丰富 | 在 __init__ 中记录关键信息,便于调试 |
| 文档字符串 | 添加文档字符串说明异常的触发条件 |
八、综合实战演练
实战一:头像格式检测
需求描述:网站只允许用户上传 jpg、png 和 jpeg 格式的文件。编写程序检测用户上传的文件扩展名,如果不符合要求则抛出异常。
涉及知识点:自定义异常、raise、endswith()、异常捕获
import os
# 自定义异常:文件格式不合法
class InvalidFileFormatError(Exception):
def __init__(self, filename, allowed_formats):
self.filename = filename
self.allowed_formats = allowed_formats
super().__init__(f"不支持的文件格式:{filename},允许的格式:{', '.join(allowed_formats)}")
def validate_image(filename):
"""验证图片格式是否合法"""
ALLOWED = ["jpg", "png", "jpeg"]
# 检查是否有扩展名
if "." not in filename:
raise InvalidFileFormatError(filename, ALLOWED)
# 获取扩展名(转小写)
ext = filename.rsplit(".", 1)[-1].lower()
if ext not in ALLOWED:
raise InvalidFileFormatError(filename, ALLOWED)
print(f"✅ 文件 {filename} 格式验证通过!")
def upload_avatar(filename):
"""上传头像"""
try:
validate_image(filename)
print(f"📤 正在上传 {filename}...")
# 模拟上传操作
print("✅ 上传成功!")
except InvalidFileFormatError as e:
print(f"❌ 上传失败:{e}")
except Exception as e:
print(f"❌ 未知错误:{e}")
# 测试
upload_avatar("avatar.jpg") # ✅ 通过
upload_avatar("photo.png") # ✅ 通过
upload_avatar("image.gif") # ❌ 不支持
upload_avatar("no_ext") # ❌ 无扩展名
运行结果:
✅ 文件 avatar.jpg 格式验证通过!
📤 正在上传 avatar.jpg...
✅ 上传成功!
✅ 文件 photo.png 格式验证通过!
📤 正在上传 photo.png...
✅ 上传成功!
❌ 上传失败:不支持的文件格式:image.gif,允许的格式:jpg, png, jpeg
❌ 上传失败:不支持的文件格式:no_ext,允许的格式:jpg, png, jpeg
实战二:海鲜超市数量检测
需求描述:用户购买海鲜时,购买数量必须不小于 1。如果用户输入的数量小于 1,程序抛出异常并提示错误信息,然后将数量设为默认值 1。
涉及知识点:try-except-else、raise、输入校验、自定义异常
class InvalidQuantityError(Exception):
"""无效数量异常"""
def __init__(self, quantity, default=1):
self.quantity = quantity
self.default = default
super().__init__(f"数量 {quantity} 无效,已自动设为默认值 {default}")
def get_quantity(prompt):
"""获取用户输入的数量,并进行校验"""
try:
quantity = int(input(prompt))
if quantity < 1:
raise InvalidQuantityError(quantity)
return quantity
except ValueError:
print("❌ 请输入有效的整数!")
return get_quantity(prompt)
except InvalidQuantityError as e:
print(f"⚠️ 警告:{e}")
return e.default
def seafood_shop():
"""海鲜超市主程序"""
print("=" * 40)
print(" 🦐 海鲜超市")
print("=" * 40)
# 海鲜价格表
products = {
"1": {"name": "龙虾", "price": 188},
"2": {"name": "帝王蟹", "price": 399},
"3": {"name": "鲍鱼", "price": 68},
"4": {"name": "三文鱼", "price": 128},
}
# 显示商品列表
print("商品列表:")
for key, product in products.items():
print(f" {key}. {product['name']}:¥{product['price']}/斤")
# 选择商品
choice = input("请选择商品编号(1-4):")
if choice not in products:
print("❌ 无效的商品编号!")
return
product = products[choice]
# 获取数量(带校验)
quantity = get_quantity(f"请输入购买数量(斤):")
# 计算总价
total = product["price"] * quantity
print("\n" + "=" * 40)
print(" 📋 购物小票")
print("=" * 40)
print(f"商品:{product['name']}")
print(f"单价:¥{product['price']}/斤")
print(f"数量:{quantity} 斤")
print(f"总价:¥{total:.2f}")
print("=" * 40)
# 运行
seafood_shop()
运行示例:
========================================
🦐 海鲜超市
========================================
商品列表:
1. 龙虾:¥188/斤
2. 帝王蟹:¥399/斤
3. 鲍鱼:¥68/斤
4. 三文鱼:¥128/斤
请选择商品编号(1-4):1
请输入购买数量(斤):-5
⚠️ 警告:数量 -5 无效,已自动设为默认值 1
========================================
📋 购物小票
========================================
商品:龙虾
单价:¥188/斤
数量:1 斤
总价:¥188.00
========================================
九、补充知识:异常处理的最佳实践
| 实践建议 | 说明 | 示例 |
|---|---|---|
| 捕获具体异常 | 不要使用裸 except:,尽量捕获具体的异常类型 |
except ValueError as e: |
| 异常粒度要适中 | 一个 try 块不要包含太多代码,尽量精准定位异常源 |
每个关键操作单独 try |
使用 finally 释放资源 |
文件、网络、数据库连接等资源在 finally 中释放 |
finally: file.close() |
| 不要忽略异常 | 捕获异常后必须有处理逻辑(记录日志、提示用户、重试等) | 不能只写 pass |
自定义异常要继承 Exception |
不要直接继承 BaseException |
class MyError(Exception): |
| 异常信息要清晰 | 异常消息应包含足够的上下文信息 | raise ValueError(f"年龄 {age} 超出范围") |
| 顶层兜底 | 在程序入口处捕获 Exception,防止程序意外崩溃 |
主循环中的 except Exception as e: |
📝 总结与知识点一览表
核心知识点汇总
| 类别 | 关键字/语法 | 核心作用 |
|---|---|---|
| 异常捕获 | try-except |
捕获并处理异常,防止程序崩溃 |
| 具体捕获 | except ValueError: |
捕获特定类型的异常 |
| 多异常捕获 | except (ValueError, TypeError): |
用元组同时捕获多种异常 |
| 异常对象 | except ValueError as e: |
获取异常对象的详细信息 |
| 无异常执行 | else: |
try 没有异常时执行 |
| 资源释放 | finally: |
无论是否异常都执行 |
| 抛出异常 | raise ValueError("msg") |
主动抛出指定异常 |
| 异常重抛 | raise |
在 except 中重新抛出当前异常 |
| 断言 | assert condition, "msg" |
条件为 False 时抛出 AssertionError |
| 自定义异常 | class MyError(Exception): |
定义业务特定的异常类型 |
| 异常传递 | 函数调用栈向上传递 | 异常未被捕获时向调用者传递 |
异常处理结构速查
| 需求 | 代码模板 |
|---|---|
| 基础捕获 | try: ... except 异常类型: ... |
| 多异常分别处理 | try: ... except A: ... except B: ... |
| 多异常合并处理 | try: ... except (A, B) as e: ... |
| 完整结构 | try: ... except: ... else: ... finally: ... |
| 主动抛异常 | raise ValueError("错误信息") |
| 断言检查 | assert 条件, "错误信息" |
| 自定义异常 | class MyError(Exception): pass |
常见异常速查表
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
ZeroDivisionError |
除数为 0 | 10 / 0 |
NameError |
使用未定义变量 | print(a)(a 未定义) |
TypeError |
类型不匹配 | "10" + 5 |
ValueError |
值不合法 | int("abc") |
IndexError |
索引越界 | [1,2][5] |
KeyError |
键不存在 | {"a":1}["b"] |
AttributeError |
属性不存在 | "hello".length |
FileNotFoundError |
文件不存在 | open("no.txt") |
ImportError |
导入失败 | import 不存在的模块 |
KeyboardInterrupt |
用户按 Ctrl+C | 中断程序执行 |
常见错误排查指南
| 错误现象 | 大概率原因 | 解决方案 |
|---|---|---|
| 程序直接崩溃并输出 Traceback | 没有 try-except 捕获异常 |
在关键代码处添加异常捕获 |
except: 捕获了所有异常但无法定位问题 |
使用了裸 except |
改为捕获具体异常或 Exception |
finally 中访问了未定义的变量 |
try 中变量创建失败(如文件打开失败) |
在 finally 中使用 locals() 或 globals() 检查变量是否存在 |
assert 在生产环境失效 |
使用了 -O 优化参数运行 |
生产环境使用 raise 替代 assert |
| 自定义异常未被捕获 | 异常类继承错误或捕获了错误的父类 | 确保自定义异常继承 Exception,且捕获时使用正确的类型 |
raise 重新抛出丢失了原始异常信息 |
使用了 raise 新异常 而非 raise |
使用 raise 不传参保留原异常,或使用 raise 新异常 from 原异常 |
学习/生产最佳实践
- 不要使用裸
except::它会捕获所有异常(包括KeyboardInterrupt和SystemExit),导致程序无法被正常中断。应使用except Exception as e:或捕获具体异常。 - 异常粒度要适中:不要在同一个
try块中包裹太多代码,否则难以定位具体是哪里出了错。每个关键操作单独使用try-except。 - 资源释放用
finally:无论是文件、数据库连接还是网络套接字,释放资源的代码应该放在finally中,确保万无一失。 - 异常信息要足够用于调试:抛出异常时提供清晰的上下文信息,如
raise ValueError(f"年龄 {age} 超出有效范围 [0, 120]")。 - 顶层兜底:在程序的入口处(如
main())设置一个顶层的try-except,捕获所有未被处理的异常,记录日志并给出友好的用户提示,而不是直接暴露 Traceback。 - 自定义异常要有层次:为项目定义一个基类异常(如
AppException),所有业务异常继承它,方便统一捕获和处理。
🔜 下期预告
本期我们系统学习了异常处理——从异常的概述与常见类型,到 try-except 捕获、else 和 finally 子句,再到 raise 主动抛出、assert 断言、异常的传递机制以及自定义异常。通过“头像格式检测”和“海鲜超市数量检测”两个实战项目,让程序在面对错误时更加健壮和可靠。
至此,Python 基础系列(第一期至第九期)已全部完结!我们完成了从环境搭建、基础语法、流程控制、字符串、组合数据类型、函数、文件与数据格式化、面向对象编程到异常处理的完整学习路径。感谢一路的坚持与陪伴!
后续,我们将正式开启自动化运维的技能学习,敬请期待下一期~
更多推荐


所有评论(0)