Python 基础语法与数据类型(十五) - 异常处理
文章目录
- 1. 什么是错误 (Error) 和异常 (Exception)?
- 2. `try`, `except`, `else`, `finally` 语句
- 2.1 `try...except` 块
- 2.2 `try...except...else` 块
- 2.3 `try...except...finally` 块
- 2.4 完整的 `try...except...else...finally` 结构
- 3. 抛出异常 (`raise`)
- 4. 自定义异常 (Custom Exceptions)
- 总结
- 练习题
- 练习题答案
创作不易,请各位看官顺手点点关注,不胜感激 。
在实际编程中,程序运行时经常会遇到各种错误或异常。例如,用户输入了无效的数据、文件不存在、网络连接中断、程序尝试除以零等等。如果不对这些情况进行妥善处理,程序就会立即崩溃,导致用户体验不佳甚至数据丢失。
本篇将深入探讨 Python 中的异常处理 (Exception Handling) 机制,它允许你的程序在遇到错误时优雅地恢复,而不是直接终止。
1. 什么是错误 (Error) 和异常 (Exception)?
在 Python 中,“错误”和“异常”这两个词通常可以互换使用,但从技术上讲,它们略有区别:
-
错误 (Error):通常指程序在编译或解释阶段就无法通过的语法错误(
SyntaxError
),或者更严重的、程序无法恢复的问题(如SystemExit
、KeyboardInterrupt
等,严格来说它们也是异常)。这类错误通常意味着程序本身存在根本性问题。# 语法错误 (SyntaxError) # print("Hello" # 缺少括号 # NameError (未定义的变量) # print(x) # x 未定义
-
异常 (Exception):指在程序运行时发生的、导致程序中断执行的事件。这些事件通常是可以预见且可以被程序捕获和处理的。Python 使用
Exception
类及其子类来表示这些运行时问题。例如:ZeroDivisionError
: 除数为零。TypeError
: 操作数类型不正确。ValueError
: 操作数类型正确但值不正确(例如,int("abc")
)。FileNotFoundError
: 文件不存在。IndexError
: 序列索引越界。KeyError
: 字典中不存在的键。
为什么需要异常处理?
想象一下你的程序是一个精密的机器。异常处理就像是这个机器的故障安全机制。当某个部件出现问题时,不是整个机器立即报废,而是会触发警报,并允许你执行预设的应对措施(例如,记录日志、通知用户、尝试修复或平稳关闭),从而避免程序的突然崩溃。
2. try
, except
, else
, finally
语句
Python 使用 try
、except
、else
和 finally
关键字来构建异常处理结构。
2.1 try...except
块
这是最基本的异常处理结构。
try
块:你认为可能引发异常的代码放在这里。except
块:如果try
块中的代码发生了指定类型的异常,except
块中的代码就会被执行。
def safe_divide(a, b):try:result = a / bprint(f"除法结果: {result}")except ZeroDivisionError:print("错误: 除数不能为零!")except TypeError:print("错误: 操作数类型不正确,请确保它们是数字。")except Exception as e: # 捕获所有其他类型的异常,并打印异常信息print(f"发生未知错误: {e}")print("--- 尝试除法操作结束 ---") # 这行会在异常处理后继续执行safe_divide(10, 2)
safe_divide(10, 0)
safe_divide(10, "a")
safe_divide([1,2], 2) # 引发 TypeError,被 except TypeError 捕获
输出:
除法结果: 5.0
--- 尝试除法操作结束 ---
错误: 除数不能为零!
--- 尝试除法操作结束 ---
错误: 操作数类型不正确,请确保它们是数字。
--- 尝试除法操作结束 ---
错误: 操作数类型不正确,请确保它们是数字。
--- 尝试除法操作结束 ---
捕获多个异常:
你可以为不同的异常类型指定不同的 except
块,也可以在一个 except
块中捕获多种异常类型,用括号 ()
包裹:
def process_data(data, index):try:value = data[index]print(f"索引 {index} 的值: {value}")# 假设这里可能引发 ValueErrorint_value = int(value)print(f"转换后的整数: {int_value}")except (IndexError, TypeError, ValueError) as e: # 捕获多种特定异常print(f"数据处理错误: {e}")except Exception as e: # 捕获所有其他未被捕获的异常print(f"发生了意料之外的错误: {e}")process_data([1, 2, 3], 1) # 正常
process_data([1, 2, 3], 5) # IndexError
process_data("hello", 0) # TypeError (字符串是可索引的,但 int("h") 会 ValueError)
process_data(["1", "2"], 0) # ValueError (int("1") 正常)
process_data(["a", "b"], 0) # ValueError (int("a") 报错)
输出:
索引 1 的值: 2
转换后的整数: 2
数据处理错误: list index out of range
索引 0 的值: h
数据处理错误: invalid literal for int() with base 10: 'h'
索引 0 的值: 1
转换后的整数: 1
索引 0 的值: a
数据处理错误: invalid literal for int() with base 10: 'a'
裸 except
(不带异常类型):
except:
块可以不指定异常类型,这样它会捕获所有类型的异常。然而,这通常不被推荐,因为它会捕获所有异常,包括那些你不应该捕获的(如 KeyboardInterrupt
、SystemExit
),这可能导致难以调试的程序行为。最好是捕获你预期的特定异常类型。
2.2 try...except...else
块
else
块:如果try
块中的代码没有发生任何异常,else
块中的代码就会被执行。它通常用于存放那些只在try
块成功执行后才需要的代码。
def get_user_number():while True:try:num_str = input("请输入一个整数: ")num = int(num_str)except ValueError:print("输入无效!这不是一个有效的整数,请重试。")else: # 如果 try 块没有引发 ValueError,则执行此块print(f"你输入了有效的整数: {num}")break # 成功输入后跳出循环get_user_number()
运行示例(用户输入):
请输入一个整数: abc
输入无效!这不是一个有效的整数,请重试。
请输入一个整数: 3.14
输入无效!这不是一个有效的整数,请重试。
请输入一个整数: 123
你输入了有效的整数: 123
2.3 try...except...finally
块
finally
块:无论try
块中是否发生异常,finally
块中的代码总是会被执行。它通常用于执行一些清理操作,例如关闭文件、释放资源、关闭网络连接等,确保这些操作无论如何都会发生。
def process_file(filename):file = None # 初始化 file 变量,防止文件未成功打开时报错try:file = open(filename, 'r')content = file.read()print(f"文件内容:\n{content}")except FileNotFoundError:print(f"错误: 文件 '{filename}' 未找到。")except Exception as e:print(f"读取文件时发生未知错误: {e}")finally:# 无论是否发生异常,都会执行关闭文件的操作if file: # 确保文件对象存在且不为 Nonefile.close()print(f"文件 '{filename}' 已关闭。")else:print(f"文件 '{filename}' 未被成功打开或无需关闭。")# 正常情况
with open("test.txt", "w") as f: # 先创建一个文件f.write("Hello, World!")
process_file("test.txt")# 文件不存在的情况
process_file("non_existent_file.txt")# 模拟读取错误(例如权限问题或文件损坏)
# 可以通过改变文件权限来模拟 IOError,此处不做复杂演示
输出:
文件内容:
Hello, World!
文件 'test.txt' 已关闭。
错误: 文件 'non_existent_file.txt' 未找到。
文件 'non_existent_file.txt' 未被成功打开或无需关闭。
注意: 对于文件操作,更推荐使用 with open(...) as f:
语句,它会自动处理文件的打开和关闭,比手动使用 finally
更简洁安全。
2.4 完整的 try...except...else...finally
结构
你可以将所有这些块组合起来,形成一个完整的异常处理流程:
def elaborate_divide(a, b):try:result = a / bexcept ZeroDivisionError:print("错误: 无法除以零。")return None # 异常发生时返回 Noneexcept TypeError:print("错误: 输入类型不正确,请提供数字。")return Noneelse: # 如果 try 块没有发生异常print(f"除法成功!结果是: {result}")return resultfinally: # 无论如何都会执行print("除法操作完成。")elaborate_divide(10, 2)
print("-" * 20)
elaborate_divide(10, 0)
print("-" * 20)
elaborate_divide(10, "x")
输出:
除法成功!结果是: 5.0
除法操作完成。
--------------------
错误: 无法除以零。
除法操作完成。
--------------------
错误: 输入类型不正确,请提供数字。
除法操作完成。
3. 抛出异常 (raise
)
有时,你的代码逻辑可能会检测到某种错误情况,但 Python 标准库没有相应的异常类型,或者你希望在特定条件下强制停止程序并发出错误信号。这时你可以使用 raise
关键字手动抛出异常。
你可以抛出 Python 内置的异常类型,也可以自定义异常。
def check_positive(number):if not isinstance(number, (int, float)):raise TypeError("输入必须是数字。")if number <= 0:raise ValueError("数字必须是正数。")print(f"{number} 是一个正数。")# 正常调用
check_positive(10)
check_positive(0.5)# 捕获自定义抛出的异常
try:check_positive(-5)
except ValueError as e:print(f"捕获到错误: {e}")try:check_positive("hello")
except TypeError as e:print(f"捕获到错误: {e}")
输出:
10 是一个正数。
0.5 是一个正数。
捕获到错误: 数字必须是正数。
捕获到错误: 输入必须是数字。
4. 自定义异常 (Custom Exceptions)
为了提高代码的可读性和可维护性,你可以根据程序的特定需求定义自己的异常类。自定义异常通常继承自 Exception
类或其子类。
class InsufficientFundsError(Exception):"""自定义异常:当余额不足时引发。"""def __init__(self, message="账户余额不足", current_balance=0, required_amount=0):super().__init__(message) # 调用父类 Exception 的构造方法self.current_balance = current_balanceself.required_amount = required_amountdef __str__(self): # 定义异常的字符串表示return f"{super().__str__()}. 当前余额: ${self.current_balance:.2f}, 需取款: ${self.required_amount:.2f}"class BankAccount:def __init__(self, owner, balance=0):self.owner = ownerself.balance = balancedef withdraw(self, amount):if amount <= 0:raise ValueError("取款金额必须大于零。")if amount > self.balance:# 余额不足时抛出自定义异常raise InsufficientFundsError(message="取款失败,余额不足。",current_balance=self.balance,required_amount=amount)self.balance -= amountprint(f"成功取出 ${amount:.2f}。当前余额: ${self.balance:.2f}")# 使用自定义异常
my_account = BankAccount("张三", 500)try:my_account.withdraw(200)my_account.withdraw(400) # 这将引发 InsufficientFundsError
except InsufficientFundsError as e:print(f"取款操作失败: {e}")
except ValueError as e:print(f"取款输入错误: {e}")
finally:print(f"张三的账户操作结束,最终余额: ${my_account.balance:.2f}")try:my_account.withdraw(-100) # 引发 ValueError
except ValueError as e:print(f"取款输入错误: {e}")
输出:
成功取出 $200.00。当前余额: $300.00
取款操作失败: 取款失败,余额不足。. 当前余额: $300.00, 需取款: $400.00
张三的账户操作结束,最终余额: $300.00
取款输入错误: 取款金额必须大于零。
自定义异常使得程序的错误处理更加语义化,便于识别和处理特定业务逻辑中的问题。
总结
异常处理是编写健壮、用户友好程序的关键。通过 try
, except
, else
, finally
关键字,你可以:
- 捕获和处理预期的错误,防止程序崩溃。
- 提供有意义的错误信息给用户或开发者。
- 确保关键的清理操作(如关闭文件)总能执行。
- 使用
raise
关键字手动抛出异常,以信号通知特定错误情况。 - 自定义异常,使错误处理更具业务语义和可读性。
熟练掌握异常处理,将使你能够编写出更稳定、更可靠的 Python 应用程序。
练习题
尝试独立完成以下练习题,并通过答案进行对照:
-
基础
try-except
:- 编写一个函数
get_list_element(data_list, index)
。 - 在函数中使用
try-except
块来尝试访问data_list
中index
位置的元素。 - 如果发生
IndexError
,打印"索引超出范围!"
。 - 如果发生
TypeError
(例如data_list
不是列表或元组),打印"数据类型不正确,期望列表或元组!"
。 - 如果没有异常,打印该元素。
- 测试:
get_list_element([10, 20, 30], 1)
get_list_element([10, 20, 30], 5)
get_list_element("hello", 2)
get_list_element(123, 0)
- 编写一个函数
-
try-except-else
(用户输入处理):- 编写一个函数
get_positive_integer()
。 - 使用一个循环,反复提示用户输入一个正整数。
- 在
try
块中,尝试将用户输入转换为整数,并检查它是否为正数。 - 如果
ValueError
(转换失败)或number <= 0
(不是正数),则在except
块中打印相应的错误信息,并让用户重试。 - 如果成功输入正整数(
else
块),打印确认信息并返回该整数。
- 编写一个函数
-
try-finally
(资源清理):- 编写一个函数
read_and_process_lines(filename)
。 - 使用
try-finally
结构,确保文件在读取完毕或发生错误后总是被关闭。 - 在
try
块中:- 尝试打开
filename
文件。 - 遍历文件中的每一行,并打印(去除首尾空格和换行符)。
- 模拟一个错误:在读取到第三行时,如果第三行包含 “error”,则
raise ValueError("模拟文件处理错误")
。
- 尝试打开
finally
块中:无论如何都要打印"文件处理完成,正在关闭文件..."
,并关闭文件(如果已打开)。- 测试:
- 创建一个
sample.txt
文件,内容如下:Line 1 Line 2 Line 3 with error Line 4
read_and_process_lines("sample.txt")
read_and_process_lines("non_existent.txt")
(此情况无需清理,但finally
块仍应执行)
- 创建一个
- 编写一个函数
-
抛出和自定义异常 (
TemperatureConverter
):- 定义一个自定义异常类
InvalidTemperatureError(Exception)
,当温度值不合理时(例如,低于绝对零度 -273.15 摄氏度)抛出。在__init__
中可以接收不合理的值。 - 定义一个
TemperatureConverter
类:- 有一个静态方法
celsius_to_fahrenheit(celsius)
,将摄氏度转换为华氏度(F = C * 9/5 + 32
)。 - 有一个静态方法
fahrenheit_to_celsius(fahrenheit)
,将华氏度转换为摄氏度(C = (F - 32) * 5/9
)。 - 在这两个转换方法中,如果输入的温度值低于绝对零度(
celsius < -273.15
或fahrenheit < -459.67
),则抛出InvalidTemperatureError
异常。
- 有一个静态方法
- 编写代码来测试这些转换:
- 正常转换一个温度。
- 尝试转换一个低于绝对零度的温度,并使用
try-except
捕获InvalidTemperatureError
。
- 定义一个自定义异常类
练习题答案
1. 基础 try-except
:
# 1. 基础 try-except
def get_list_element(data_list, index):"""尝试访问列表中指定索引的元素,并处理可能发生的异常。"""print(f"\n尝试访问列表/序列: {data_list}, 索引: {index}")try:element = data_list[index]print(f"成功获取元素: {element}")except IndexError:print("索引超出范围!")except TypeError:print("数据类型不正确,期望列表或元组!")except Exception as e: # 捕获其他所有未预料的异常print(f"发生未知错误: {e}")# 测试
get_list_element([10, 20, 30], 1)
get_list_element([10, 20, 30], 5)
get_list_element("hello", 2)
get_list_element(123, 0) # 整数不可索引,引发 TypeError
输出:
尝试访问列表/序列: [10, 20, 30], 索引: 1
成功获取元素: 20尝试访问列表/序列: [10, 20, 30], 索引: 5
索引超出范围!尝试访问列表/序列: hello, 索引: 2
成功获取元素: l尝试访问列表/序列: 123, 索引: 0
数据类型不正确,期望列表或元组!
2. try-except-else
(用户输入处理):
# 2. try-except-else (用户输入处理)
def get_positive_integer():"""反复提示用户输入一个正整数,直到输入有效。Returns:int: 用户输入的有效正整数。"""while True:try:num_str = input("请输入一个正整数: ")num = int(num_str) # 尝试转换为整数if num <= 0:# 即使转换成功,如果不是正数,也作为 ValueError 处理raise ValueError("输入必须是正数。")except ValueError as e: # 捕获 int() 转换失败或自定义的正数检查失败print(f"输入无效!{e} 请重试。")except Exception as e: # 捕获其他意外错误print(f"发生未知错误: {e}")else: # 如果 try 块没有引发任何异常print(f"恭喜!你输入了有效的正整数: {num}")return num # 返回有效整数并跳出循环# 调用函数
user_input_num = get_positive_integer()
print(f"程序接收到的正整数是: {user_input_num}")
运行示例(用户输入):
请输入一个正整数: abc
输入无效!invalid literal for int() with base 10: 'abc' 请重试。
请输入一个正整数: 0
输入无效!输入必须是正数。 请重试。
请输入一个正整数: -10
输入无效!输入必须是正数。 请重试。
请输入一个正整数: 3.14
输入无效!invalid literal for int() with base 10: '3.14' 请重试。
请输入一个正整数: 42
恭喜!你输入了有效的正整数: 42
程序接收到的正整数是: 42
3. try-finally
(资源清理):
# 3. try-finally (资源清理)
import osdef read_and_process_lines(filename):"""读取文件内容,处理每一行,并在完成后确保文件关闭。Args:filename (str): 要读取的文件名。"""file_obj = None # 初始化文件对象为 Noneprint(f"\n--- 尝试处理文件: {filename} ---")try:file_obj = open(filename, 'r', encoding='utf-8')line_num = 0for line in file_obj:line_num += 1processed_line = line.strip()print(f"读取到第 {line_num} 行: '{processed_line}'")if line_num == 3 and "error" in processed_line.lower():raise ValueError("模拟文件处理错误:第三行包含 'error'。") # 模拟错误except FileNotFoundError:print(f"错误: 文件 '{filename}' 未找到。")except ValueError as e:print(f"处理文件时捕获到错误: {e}")except Exception as e:print(f"发生意外错误: {e}")finally:# 无论 try 块中是否发生异常,此块总是会执行print(f"文件处理完成,正在关闭文件 (如果已打开)...")if file_obj: # 只有当 file_obj 成功赋值后才尝试关闭file_obj.close()print(f"文件 '{filename}' 已成功关闭。")else:print(f"文件 '{filename}' 未成功打开,无需关闭。")# 创建一个示例文件
sample_content = """
Line 1
Line 2
Line 3 with error
Line 4
"""
with open("sample.txt", "w", encoding='utf-8') as f:f.write(sample_content.strip())# 测试:正常情况 (会遇到模拟错误)
read_and_process_lines("sample.txt")# 测试:文件不存在的情况
read_and_process_lines("non_existent.txt")# 清理测试文件
if os.path.exists("sample.txt"):os.remove("sample.txt")
输出:
--- 尝试处理文件: sample.txt ---
读取到第 1 行: 'Line 1'
读取到第 2 行: 'Line 2'
读取到第 3 行: 'Line 3 with error'
处理文件时捕获到错误: 模拟文件处理错误:第三行包含 'error'。
文件处理完成,正在关闭文件 (如果已打开)...
文件 'sample.txt' 已成功关闭。--- 尝试处理文件: non_existent.txt ---
错误: 文件 'non_existent.txt' 未找到。
文件处理完成,正在关闭文件 (如果已打开)...
文件 'non_existent.txt' 未成功打开,无需关闭。
4. 抛出和自定义异常 (TemperatureConverter
):
# 4. 抛出和自定义异常 (TemperatureConverter)
class InvalidTemperatureError(Exception):"""自定义异常:当温度值不合理 (低于绝对零度) 时引发。"""def __init__(self, value, unit):self.value = valueself.unit = unitmessage = f"无效温度: {value:.2f} {unit}。温度不能低于绝对零度。"super().__init__(message) # 调用父类 Exception 的构造方法class TemperatureConverter:# 绝对零度常量ABSOLUTE_ZERO_CELSIUS = -273.15ABSOLUTE_ZERO_FAHRENHEIT = -459.67@staticmethoddef celsius_to_fahrenheit(celsius):"""将摄氏度转换为华氏度。Args:celsius (float): 摄氏温度。Returns:float: 华氏温度。Raises:InvalidTemperatureError: 如果摄氏度低于绝对零度。"""if celsius < TemperatureConverter.ABSOLUTE_ZERO_CELSIUS:raise InvalidTemperatureError(celsius, "摄氏度")fahrenheit = celsius * 9/5 + 32return fahrenheit@staticmethoddef fahrenheit_to_celsius(fahrenheit):"""将华氏度转换为摄氏度。Args:fahrenheit (float): 华氏温度。Returns:float: 摄氏温度。Raises:InvalidTemperatureError: 如果华氏度低于绝对零度。"""if fahrenheit < TemperatureConverter.ABSOLUTE_ZERO_FAHRENHEIT:raise InvalidTemperatureError(fahrenheit, "华氏度")celsius = (fahrenheit - 32) * 5/9return celsius# 测试正常转换
print("--- 正常温度转换 ---")
c_temp = 25.0
f_temp = TemperatureConverter.celsius_to_fahrenheit(c_temp)
print(f"{c_temp}°C = {f_temp:.2f}°F")f_temp2 = 77.0
c_temp2 = TemperatureConverter.fahrenheit_to_celsius(f_temp2)
print(f"{f_temp2}°F = {c_temp2:.2f}°C")# 测试低于绝对零度的温度 (捕获异常)
print("\n--- 异常温度转换 ---")
try:extreme_c = -300.0 # 低于绝对零度f = TemperatureConverter.celsius_to_fahrenheit(extreme_c)print(f"{extreme_c}°C = {f:.2f}°F")
except InvalidTemperatureError as e:print(f"捕获到异常: {e}")try:extreme_f = -500.0 # 低于绝对零度c = TemperatureConverter.fahrenheit_to_celsius(extreme_f)print(f"{extreme_f}°F = {c:.2f}°C")
except InvalidTemperatureError as e:print(f"捕获到异常: {e}")
输出:
--- 正常温度转换 ---
25.0°C = 77.00°F
77.0°F = 25.00°C--- 异常温度转换 ---
捕获到异常: 无效温度: -300.00 摄氏度。温度不能低于绝对零度。
捕获到异常: 无效温度: -500.00 华氏度。温度不能低于绝对零度。