
文章目录
📖 开篇导读
前几节课我们学习了文件操作,代码越来越复杂,程序在任何环节都可能出错:用户输入了非数字、文件不存在、网络连接断开、除数为零、内存不足……这些错误在Python中被称为异常。如果不处理异常,程序就会直接崩溃,并输出一大串红色的错误信息,这对用户来说是无法接受的。
优秀的程序不仅要实现功能,还要具备容错性——能够优雅地处理异常,给出有用的提示,然后继续运行或安全退出。异常处理正是实现这一目标的关键技术。
💡 工作场景:后端接口需要捕获数据库查询异常并返回友好提示;文件处理时要处理文件不存在的异常;用户输入验证要捕获类型转换错误;网络请求要处理超时和连接异常。异常处理让程序更健壮,也更容易调试。
本课我们将学习:
- 异常的概念和常见异常类型
try-except-else-finally结构- 捕获多个异常与异常链
- 抛出异常(
raise)与自定义异常 assert断言的使用- 最佳实践与性能注意事项
学完本课,你将能够编写出防御性强、用户体验好的健壮程序。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 理解异常的概念和常见异常类型 | 读懂错误信息,快速定位问题 |
| 2️⃣ | 掌握try-except捕获异常的基本用法 | 防止程序因异常崩溃 |
| 3️⃣ | 熟练使用else和finally子句 | 资源释放、异常后处理 |
| 4️⃣ | 学会抛出异常(raise)和自定义异常 | 编写健壮的库和API |
| 5️⃣ | 掌握assert断言进行调试和契约检查 | 开发阶段及早发现问题 |
| 6️⃣ | 了解异常链和最佳实践 | 写出清晰的错误处理代码 |
🔥 面试考点:“
try-except-else-finally的执行顺序?”“raise不带参数的作用?”“自定义异常如何实现?”“assert与异常的区别?”
📚 知识点理论精讲
一、什么是异常?
异常是程序运行时发生的错误,它会中断正常的代码执行流程。Python遇到异常时,会创建一个异常对象,并查找处理它的代码。如果未找到,程序崩溃并输出Traceback。
1.1 异常 vs 语法错误
- 语法错误:代码不符合Python语法,解释器无法解析,会在程序执行前报错。
- 异常:语法正确,但运行时发生了错误(如除零、文件不存在)。
# 语法错误
# print "hello" # SyntaxError: Missing parentheses
# 异常
print(1 / 0) # ZeroDivisionError
1.2 常见异常类型
| 异常 | 触发场景 |
|---|---|
ZeroDivisionError | 除数为0 |
TypeError | 不同类型运算,如"5" + 3 |
ValueError | 值转换错误,如int("abc") |
IndexError | 索引超出序列范围 |
KeyError | 字典键不存在 |
FileNotFoundError | 文件不存在 |
AttributeError | 对象没有该属性 |
NameError | 变量未定义 |
IOError | 输入/输出错误(老版,现在基本用OSError) |
ImportError | 导入模块失败 |
二、try-except 基础
2.1 基本语法
try:
# 可能会抛出异常的代码
risky_code()
except 异常类型:
# 发生异常时执行的代码
handle_error()
try:
num = int(input("请输入数字: "))
result = 100 / num
print(f"结果是: {result}")
except ValueError:
print("输入无效,请输入数字")
except ZeroDivisionError:
print("数字不能为零")
2.2 捕获多个异常
可以用元组在一个except中捕获多种异常:
try:
data = {"name": "张三"}
value = data["age"] # KeyError
result = 100 / int(value)
except (KeyError, ValueError) as e:
print(f"数据错误: {e}")
2.3 捕获所有异常(不推荐)
try:
risky()
except Exception as e: # 捕获所有非系统退出的异常
print(f"发生错误: {e}")
⚠️ 注意:尽量不要捕获
BaseException或空except:,因为它会捕获KeyboardInterrupt、SystemExit等,导致无法正常退出程序。通常捕获Exception或其子类。
2.4 获取异常对象
使用as关键字可以获取异常实例,获取详细信息。
try:
f = open("missing.txt")
except FileNotFoundError as e:
print(f"错误: {e}")
print(f"错误号: {e.errno}")
print(f"文件名: {e.filename}")
三、else 和 finally 子句
3.1 else:没有异常时执行
else子句中的代码在try块没有发生异常时执行。适合放置那些依赖于try成功后才能执行的代码。
try:
f = open("data.txt", "r")
except FileNotFoundError:
print("文件不存在")
else:
content = f.read()
print(f"文件长度: {len(content)}")
f.close()
3.2 finally:无论是否异常都执行
finally子句中的代码一定会执行,通常用于清理资源(如关闭文件、释放锁)。
f = None
try:
f = open("data.txt", "r")
content = f.read()
except FileNotFoundError:
print("文件不存在")
finally:
if f:
f.close()
print("文件已关闭")
注意:finally会在return之前执行。即使try或except中使用了return,finally仍会执行后再返回。
def test():
try:
return 1
finally:
print("finally执行了")
# 返回1,但先打印"finally执行了"
3.3 完整的try-except-else-finally
执行顺序:
- 执行
try块 - 如果发生异常,跳过
try剩余部分,匹配except - 如果没有异常,执行
else块 - 无论是否异常,最后执行
finally块
try:
x = 1 / 1 # 正常
except ZeroDivisionError:
print("除零错误")
else:
print("没发生异常")
finally:
print("总是执行")
# 输出: 没发生异常 -> 总是执行
四、抛出异常(raise)
4.1 主动抛出异常
可以使用raise语句抛出异常。可以抛出内置异常,也可以抛出自定义异常。
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
4.2 raise不带参数
在except块中,raise不带参数会重新抛出当前捕获的异常,保留原始调用栈。
try:
risky_call()
except SomeError:
print("记录日志...")
raise # 重新抛出,上层可以继续捕获
4.3 异常链(raise ... from ...)
Python 3支持异常链,用于在转换异常时保留原始异常信息。
try:
int("abc")
except ValueError as e:
raise TypeError("转换失败") from e
# 可以抑制原始异常:raise TypeError("转换失败") from None
五、自定义异常
通过继承Exception类(或其子类)创建自定义异常。通常只添加文档字符串,无需额外方法。
class ValidationError(Exception):
"""输入验证错误"""
pass
class NegativeNumberError(ValueError):
"""负数错误"""
def __init__(self, value, message="数值不能为负数"):
self.value = value
self.message = message
super().__init__(f"{message}: {value}")
使用自定义异常可以让错误类型更有语义。
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
六、assert断言
assert用于调试目的,检查条件是否为真。如果条件为假,抛出AssertionError。
assert condition, "错误信息"
示例:
def calculate_average(scores):
assert len(scores) > 0, "列表不能为空"
return sum(scores) / len(scores)
特点:
- 可以在运行Python时使用
-O(优化)标志禁用断言。 - 不要依赖断言做业务逻辑验证,因为它们可以被关闭。
- 适用于开发阶段测试不变条件。
七、异常处理的最佳实践
- 只捕获你能够处理的异常:无法处理的异常应该让它向上传播。
- 在合适层级捕获:底层函数往往不捕获,由上层统一处理。
- 尽量精确捕获异常:不要使用裸露的
except:或捕获BaseException。 - 使用
finally释放资源:但更好的方式是使用with上下文管理器。 - 不要忽略异常:空
except会隐藏错误,至少记录日志。 - 抛出有意义的异常:提供清晰的错误信息。
- 使用自定义异常区分业务错误。
💻 代码案例实操
案例1:基本异常捕获与用户输入处理
"""
input_validation.py
演示用户输入验证,捕获各种异常
"""
def get_positive_integer(prompt):
"""循环获取正整数"""
while True:
try:
value = input(prompt)
if value.lower() == 'quit':
return None
num = int(value)
if num <= 0:
raise ValueError("数字必须为正整数")
return num
except ValueError as e:
print(f"输入错误: {e},请重新输入")
except KeyboardInterrupt:
print("\n用户取消输入")
return None
def safe_division():
a = get_positive_integer("请输入被除数: ")
if a is None:
return
b = get_positive_integer("请输入除数: ")
if b is None:
return
try:
result = a / b
print(f"{a} / {b} = {result:.2f}")
except ZeroDivisionError:
print("除数不能为零,但已经捕获不会再发生") # 由于get_positive_integer确保b>0,实际上不会
except Exception as e:
print(f"未知错误: {e}")
safe_division()
案例2:文件操作的健壮处理
"""
file_error_handling.py
演示文件读写中的异常处理
"""
def read_file_safe(filename):
"""安全读取文件内容,返回内容或None"""
try:
with open(filename, 'r', encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
print(f"错误:文件 {filename} 不存在")
except PermissionError:
print(f"错误:没有权限读取 {filename}")
except UnicodeDecodeError as e:
print(f"编码错误:{e}")
except Exception as e:
print(f"读取文件时发生未知错误:{e}")
return None
def append_to_file(filename, content):
"""安全追加内容到文件"""
try:
with open(filename, 'a', encoding='utf-8') as f:
f.write(content + "\n")
print("写入成功")
return True
except Exception as e:
print(f"写入失败: {e}")
return False
# 测试
data = read_file_safe("test.txt")
if data is None:
append_to_file("test.txt", "初始内容")
data = read_file_safe("test.txt")
if data:
print(f"文件内容: {data}")
案例3:else和finally演示——数据库模拟
"""
else_finally_demo.py
演示else和finally的使用场景
"""
import random
def simulate_db_operation():
"""模拟数据库查询,成功返回数据,失败抛出异常"""
if random.random() < 0.3:
raise ConnectionError("数据库连接失败")
if random.random() < 0.2:
raise ValueError("查询语句错误")
return [("张三", 25), ("李四", 30)]
def fetch_data():
conn = None
try:
# 模拟打开连接
conn = "connection_object"
print("数据库已连接")
data = simulate_db_operation()
except ConnectionError as e:
print(f"连接错误: {e}")
return []
except ValueError as e:
print(f"查询错误: {e}")
return []
else:
# 没有异常时处理数据
print(f"成功获取 {len(data)} 条数据")
return data
finally:
# 无论是否异常,都关闭连接
if conn:
print("数据库连接已关闭")
result = fetch_data()
print(f"结果: {result}")
案例4:抛出异常与自定义异常
"""
custom_exception.py
演示自定义异常和主动抛出
"""
class BankAccountError(Exception):
"""银行账户基础异常"""
pass
class InsufficientFundsError(BankAccountError):
"""余额不足异常"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足: 当前余额 {balance},需要 {amount}")
class NegativeAmountError(BankAccountError):
"""负数金额异常"""
pass
class BankAccount:
def __init__(self, initial=0):
if initial < 0:
raise NegativeAmountError(f"初始余额不能为负数: {initial}")
self.balance = initial
def deposit(self, amount):
if amount <= 0:
raise NegativeAmountError(f"存款金额必须为正: {amount}")
self.balance += amount
print(f"存入 {amount},当前余额 {self.balance}")
def withdraw(self, amount):
if amount <= 0:
raise NegativeAmountError(f"取款金额必须为正: {amount}")
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
print(f"取出 {amount},当前余额 {self.balance}")
# 使用
account = BankAccount(100)
try:
account.deposit(50)
account.withdraw(200)
except InsufficientFundsError as e:
print(f"交易失败: {e}")
except NegativeAmountError as e:
print(f"金额错误: {e}")
except BankAccountError as e:
print(f"银行错误: {e}")
案例5:assert断言的使用
"""
assert_demo.py
演示断言在开发和调试中的作用
"""
def calculate_bmi(weight, height):
"""计算BMI,height以米为单位"""
assert weight > 0, "体重必须为正"
assert height > 0, "身高必须为正"
bmi = weight / (height ** 2)
# 物理上BMI不可能超过100(实际极少超过50)
assert bmi < 100, f"计算结果异常: BMI={bmi}"
return bmi
# 正常调用
print(calculate_bmi(70, 1.75))
# 触发断言
try:
print(calculate_bmi(-70, 1.75))
except AssertionError as e:
print(f"断言失败: {e}")
# 禁用断言运行:python -O assert_demo.py
案例6:异常链和重新抛出
"""
exception_chain.py
演示raise from和重新抛出
"""
def parse_data(data_str):
try:
return int(data_str)
except ValueError as e:
raise ValueError("数据格式错误: 期望整数") from e
def process():
try:
value = parse_data("abc")
except ValueError as e:
print(f"捕获到: {e}")
print(f"原始异常: {e.__cause__}")
# 重新抛出,让上层处理
raise
def main():
try:
process()
except ValueError as e:
print(f"最终捕获: {e}")
# 抑制原始异常
# raise ValueError("处理失败") from None
if __name__ == "__main__":
main()
案例7:综合实战——健壮的配置加载器
"""
config_loader.py
模拟从文件加载配置,具有完善的异常处理
"""
import json
import os
class ConfigError(Exception):
"""配置相关异常基类"""
pass
class ConfigNotFoundError(ConfigError):
"""配置文件不存在"""
pass
class ConfigFormatError(ConfigError):
"""配置文件格式错误"""
pass
def load_config(filepath, default_config=None):
"""
安全加载JSON配置文件。
如果文件不存在,返回默认配置。
如果格式错误,尝试修复或抛出异常。
"""
if default_config is None:
default_config = {}
try:
with open(filepath, 'r', encoding='utf-8') as f:
config = json.load(f)
print(f"成功加载配置: {filepath}")
return config
except FileNotFoundError:
print(f"配置文件不存在: {filepath},使用默认配置")
return default_config
except json.JSONDecodeError as e:
print(f"配置文件JSON解析错误: {e}")
# 可以尝试备份损坏的文件等
raise ConfigFormatError(f"配置格式错误: {e}") from e
except PermissionError as e:
raise ConfigError(f"无权限读取配置文件: {e}") from e
except Exception as e:
raise ConfigError(f"加载配置时发生未知错误: {e}") from e
def save_config(config, filepath):
"""安全保存配置到文件"""
try:
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4, ensure_ascii=False)
print(f"配置已保存: {filepath}")
except PermissionError as e:
raise ConfigError(f"无权限写入配置文件: {e}") from e
except Exception as e:
raise ConfigError(f"保存配置失败: {e}") from e
# 使用示例
config_file = "config.json"
try:
config = load_config(config_file, {"debug": False, "port": 8080})
print("当前配置:", config)
# 修改配置
config["debug"] = True
save_config(config, config_file)
except ConfigError as e:
print(f"配置操作失败: {e}")
# 清理测试文件
if os.path.exists(config_file):
os.remove(config_file)
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 使用空except: | 会捕获KeyboardInterrupt、SystemExit等,导致Ctrl+C无法退出 | 使用except Exception:或捕获具体异常 |
| 2 | 捕获异常后不做任何处理 | 错误被隐藏,难以调试 | 至少记录日志或打印信息 |
| 3 | 在finally中return | 会覆盖try或except中的return值,导致意外结果 | 不要在finally中使用return |
| 4 | 没有遵循try块只包含可能抛异常的代码 | 捕获范围过大,可能掩盖无关错误 | 仅把可能出错的代码放在try中 |
| 5 | raise不带参数用在except块外 | 引发RuntimeError | 只有活跃异常时才能无参raise |
| 6 | 依赖断言做业务逻辑验证 | 断言可被优化开关禁用,生产环境失效 | 使用if判断并抛出适当的异常 |
| 7 | 捕获异常后重新抛出时丢失原始调用栈 | 调试困难 | 使用raise不带参数,或raise ... from e |
| 8 | 自定义异常未继承Exception | 不会被通用except Exception捕获 | 继承Exception或其子类 |
| 9 | 在except块中访问可能未定义的变量 | NameError | 确保变量在try块中已定义,或在except中定义默认值 |
| 10 | 嵌套try-except过多 | 代码难以阅读 | 将异常处理封装到函数中,使用with管理资源 |
📝 课后实战练习题
第1题:基础异常捕获
编写一个程序,要求用户输入两个整数,计算它们的除法结果。处理以下异常:
- 输入非数字时提示重新输入
- 除数为0时提示不能为0
- 使用循环直到用户输入正确或输入’quit’退出
第2题:文件读取与异常
编写函数read_number_from_file(filename, line_number),读取文本文件中指定行的内容,并将其转换为整数返回。需要处理:
- 文件不存在
- 行号超出范围
- 该行内容不是有效整数
分别针对不同异常返回不同的错误码或抛出不同异常。
第3题:else和finally实践
模拟网络请求:编写函数fetch_https://blog.csdn.net/url(url),可能抛出ConnectionError、TimeoutError。在调用处使用try-except-else-finally,无论是否成功都打印“请求结束”,成功时打印响应数据长度。
第4题:自定义异常
设计一个简单的用户注册系统,要求用户名长度在3-10之间,密码长度至少6位。如果不符合条件,抛出自定义的ValidationError异常(包含具体原因)。在主程序中捕获并打印友好提示。
第5题:异常链
编写三个函数:func1调用func2,func2调用func3,func3可能抛出ValueError。在func1中捕获ValueError并将其转换为自定义异常AppError,保留原始异常信息(使用from)。测试并打印异常链。
第6题:断言与调试
写一个函数sort_and_remove_duplicates(lst),使用断言确保传入的是列表,且元素是可哈希的。然后实现功能并返回新列表。演示当传入无效参数时断言失败。
第7题:综合——健壮的配置管理器
实现一个配置管理器类ConfigManager,支持从JSON文件加载和保存。提供get(key, default)和set(key, value)方法。内部使用异常处理处理文件IO错误、JSON解析错误。如果配置文件损坏,则备份损坏文件并创建新的配置文件。编写测试用例模拟各种错误情况。
🧠 知识点思维导图总结
🔜 下节课预告
异常处理让程序更健壮,而实际开发中经常需要处理日期时间、随机数等通用功能。Python提供了丰富的内置模块,下一节课我们将学习三个常用模块。
第19课:时间模块、随机模块、系统模块常用功能零基础实战
内容包括:
datetime模块:日期时间运算、格式化、解析time模块:时间戳、睡眠、性能计时random模块:随机整数、随机选择、洗牌、随机种子sys模块:命令行参数、标准输入输出、递归深度- 实战:生成随机验证码、计时器装饰器、进度条模拟
这些模块是编写实用工具必不可少的,学完你的工具箱会更加完善。
🌟 学习鼓励:异常处理看似“防御性编程”,但它是专业程序员和业余爱好者的重要分水岭。你可能觉得“我的代码不会出错”,但用户和环境总会出乎意料。从现在开始,在编写可能出错的地方都考虑异常处理,你会写出更可靠的程序。加油!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
290

被折叠的 条评论
为什么被折叠?



