第 18 课 - 异常处理(try-except-finally)
1. 学习目标
完成本课学习后,你将能够:
- 理解程序中异常(错误)产生的原因和影响。
- 熟练使用
try-except语句块来捕获和处理特定类型的异常。 - 掌握
finally子句的作用,并理解其与try-except的协同工作流程。 - 学会使用
else子句,明确区分正常执行与异常处理的代码路径。 - 编写能够优雅应对运行时错误、更加健壮可靠的 Python 程序。
2. 核心概念
什么是异常?
在编写和运行程序时,错误不可避免。Python 将程序运行时发生的错误称为 异常。例如,尝试打开一个不存在的文件、将一个字符串与一个数字相加、或者除以零,都会引发异常。如果异常未被处理,程序通常会崩溃并停止执行。
异常处理:try-except
为了让程序在遇到错误时不会直接"罢工",我们需要 异常处理。基本思路是:把可能出错的代码放在 try(尝试)块里,然后用 except(除了)块来捕获并处理可能发生的错误。
这就像给你的程序上了个"保险"。try 里的代码正常执行;如果中途出了岔子(抛出异常),程序会立刻跳转到对应的 except 块执行补救措施,而不是直接退出。
资源清理:finally
有时候,无论程序执行成功还是失败,我们都必须执行一些"清理"动作,比如关闭文件、断开网络连接或释放锁。finally(最终)子句就是为此而生。无论 try 块中是否发生异常,finally 子句中的代码块都会被执行。
正常路径:else
有时,我们希望在 try 块中的代码 没有 引发任何异常时,才执行某段代码。else(否则)子句正是为了这个目的。else 子句只有在 try 块成功执行完毕后才会运行。
完整流程:
try块中的语句被执行。- 如果发生异常,跳转到匹配的
except块。 - 如果没有发生异常,执行
else块(如果存在)。 - 无论是否发生异常,最后都执行
finally块(如果存在)。
3. 代码示例
以下示例演示了如何结合 try-except-else-finally 处理一个常见的文件读取操作。我们尝试读取一个文件,并处理可能出现的文件不存在错误。
# 异常处理示例:安全地读取文件
def read_file_safely(filename):
"""
尝试安全地读取指定文件内容。
处理文件不存在(FileNotFoundError)的情况。
"""
file_handle = None # 初始化文件句柄
try:
# 尝试打开文件
print(f"尝试打开文件: {filename}")
file_handle = open(filename, 'r') # 这行可能引发 FileNotFoundError
content = file_handle.read()
print(f"文件读取成功!")
except FileNotFoundError as e:
# 专门捕获文件不存在的错误
print(f"错误:找不到文件 '{filename}'!")
print(f"详细信息: {e}")
content = None # 可以选择返回一个默认值
except Exception as e:
# 捕获其他未知的错误 (使用宽泛的Exception需谨慎,这里仅作演示)
print(f"发生了一个未预料的错误: {e}")
content = None
else:
# 只有在 try 块中的 open 和 read 都成功时,才会执行这里
print(f"成功读取了 {len(content)} 个字符。")
return content # 返回读取到的内容
finally:
# 无论前面发生了什么,这里总会执行
# 确保文件被正确关闭,避免资源泄露
if file_handle:
print("在 finally 中关闭文件...")
file_handle.close()
else:
print("finally 执行:没有打开的文件需要关闭。")
print("清理工作完成。\n")
# 测试1:尝试读取一个不存在的文件
print("--- 测试 1: 文件不存在 ---")
result = read_file_safely("non_existent_file.txt")
print(f"函数返回值: {result}")
# 测试2:读取一个存在的文件 (你需要先创建这个文件)
# 创建一个测试文件
with open("test_file.txt", "w") as f:
f.write("Hello, Exception Handling!\nThis is a test file.")
print("--- 测试 2: 文件存在 ---")
result = read_file_safely("test_file.txt")
print(f"函数返回值: {result}")
运行结果预测:
- 测试 1:会触发
FileNotFoundError,执行对应的except块和finally块。 - 测试 2:如果文件存在,将成功执行
try块和else块,最后执行finally块。
4. 实践练习
练习 1:基础除法运算
编写一个函数 safe_divide(a, b),它接受两个参数 a 和 b,尝试计算并返回 a / b。需要处理两种可能的错误:
b为0导致的ZeroDivisionError。- 参数类型不是数字导致的
TypeError。 在发生错误时,返回一个明确的字符串(如 "除数不能为零" 或 "参数类型错误")。
练习 2:用户输入验证
编写一个程序,要求用户输入一个数字。程序需要捕获用户输入非数字字符串(如 "abc")时引发的 ValueError。使用循环,直到用户输入一个有效的数字为止,并最终打印该数字的平方。
预期输出示例:
请输入一个数字: hello
输入错误,请输入一个有效的数字。
请输入一个数字: 12.5
你输入的数字是 12.5,它的平方是 156.25
练习 3:组合练习 - 读取配置并计算
编写一个函数 process_config(filename)。它尝试从指定的文件中读取两行,第一行是数字 x,第二行是数字 y。函数需要:
- 处理文件不存在的错误。
- 处理文件内容格式错误(某一行不是有效数字)的错误。
- 处理除数为零的错误。
- 如果一切正常,计算并返回
x / y的结果。
5. 常见错误
- "吞掉"异常:在
except块中只写一个pass或打印一条非常模糊的消息(如 "出错了"),而不记录任何具体信息。这会使调试变得极其困难。try: risky_operation() except: pass # 非常糟糕的做法!忽略了所有错误。 - 捕获过于宽泛的异常:直接使用
except Exception或except:(捕获所有)可能会掩盖真正的 bug。应优先捕获具体的、可预见的异常类型。 - 忽略资源清理:忘记在
finally中关闭文件或释放网络连接等,可能导致资源泄露和程序异常。 - 在
try块外使用except:except必须紧跟在try块之后,不能单独存在。 - 混淆
else和finally的时机:else仅在没有异常时执行,finally无论如何都会执行。
6. 小结
- 异常处理机制:
try-except-finally结构是编写健壮 Python 程序的核心。它让程序能够优雅地处理意外情况,而不是直接崩溃。 try块:放置可能引发异常的代码。except块:捕获并处理特定类型的异常。可以写多个except块来处理不同类型的错误。else块:当try块没有发生任何异常时执行的代码。用于清晰地分离"正常逻辑"。finally块:无论是否发生异常,总会执行的代码。主要用于释放资源(如关闭文件、网络连接)。- 最佳实践:尽量捕获具体的异常类型,并为用户提供有意义的错误信息,而不是简单地忽略错误。养成使用
finally进行资源清理的好习惯。
下一课,我们将学习如何定义自己的异常类型(自定义异常)以及如何链接异常,这能让你的错误处理代码更具表现力和可维护性。