当前位置: 首页 > news >正文

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),或者更严重的、程序无法恢复的问题(如 SystemExitKeyboardInterrupt 等,严格来说它们也是异常)。这类错误通常意味着程序本身存在根本性问题。

    # 语法错误 (SyntaxError)
    # print("Hello" # 缺少括号
    # NameError (未定义的变量)
    # print(x) # x 未定义
    
  • 异常 (Exception):指在程序运行时发生的、导致程序中断执行的事件。这些事件通常是可以预见且可以被程序捕获和处理的。Python 使用 Exception 类及其子类来表示这些运行时问题。例如:

    • ZeroDivisionError: 除数为零。
    • TypeError: 操作数类型不正确。
    • ValueError: 操作数类型正确但值不正确(例如,int("abc"))。
    • FileNotFoundError: 文件不存在。
    • IndexError: 序列索引越界。
    • KeyError: 字典中不存在的键。

为什么需要异常处理?

想象一下你的程序是一个精密的机器。异常处理就像是这个机器的故障安全机制。当某个部件出现问题时,不是整个机器立即报废,而是会触发警报,并允许你执行预设的应对措施(例如,记录日志、通知用户、尝试修复或平稳关闭),从而避免程序的突然崩溃。


2. try, except, else, finally 语句

Python 使用 tryexceptelsefinally 关键字来构建异常处理结构。

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: 块可以不指定异常类型,这样它会捕获所有类型的异常。然而,这通常不被推荐,因为它会捕获所有异常,包括那些你不应该捕获的(如 KeyboardInterruptSystemExit),这可能导致难以调试的程序行为。最好是捕获你预期的特定异常类型。

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 应用程序。


练习题

尝试独立完成以下练习题,并通过答案进行对照:

  1. 基础 try-except

    • 编写一个函数 get_list_element(data_list, index)
    • 在函数中使用 try-except 块来尝试访问 data_listindex 位置的元素。
    • 如果发生 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)
  2. try-except-else (用户输入处理):

    • 编写一个函数 get_positive_integer()
    • 使用一个循环,反复提示用户输入一个正整数。
    • try 块中,尝试将用户输入转换为整数,并检查它是否为正数。
    • 如果 ValueError(转换失败)或 number <= 0(不是正数),则在 except 块中打印相应的错误信息,并让用户重试。
    • 如果成功输入正整数(else 块),打印确认信息并返回该整数。
  3. 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 块仍应执行)
  4. 抛出和自定义异常 (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.15fahrenheit < -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 华氏度。温度不能低于绝对零度。
http://www.dtcms.com/a/291530.html

相关文章:

  • 把sudo搞坏了怎么修复:报错sudo: /etc/sudo.conf is owned by uid 1000, should be 0
  • 小孙学变频学习笔记(十一)关于V/F曲线的讨论
  • vue3+element-plus,el-autocomplete远程搜索,解决下拉框闪一下的问题
  • 概率论与数理统计(八)
  • Java IO 流详解:从基础到实战,彻底掌握输入输出编程
  • 自定义命令行解释器shell
  • Android开发中Crash治理方案
  • C++中的detach
  • Python打卡Day20 常见的特征筛选算法
  • C 语言的指针复习笔记
  • 圆柱电池自动分选机:全流程自动化检测的革新之路
  • 大模型中的Actor-Critic机制
  • 嵌入式学习笔记--MCU阶段--DAY08总结
  • 【Java基础03】Java变量2
  • seata at使用
  • 自然语言推理技术全景图:从基准数据集到BERT革命
  • 设备虚拟化技术-IRF
  • 利用DeepSeek编写批量输出多个主机的磁盘状况的脚本
  • 携“养鲜”魔法赴中卫,容声冰箱让石头缝里的鲜甜走得更远
  • 前端之学习后端java小白(一)之SDKMAN及helloword
  • EcoVadis评估:为企业带来的多重价值与竞争优势
  • QT跨平台应用程序开发框架(11)—— Qt系统相关
  • STM32F1使用volatile关键字避免内存优化
  • 基于springboot+vue开发的图书馆座位预约系统【源码+sql+可运行】【50721
  • 在安卓开发中,多次点击启动 Service 会有什么问题?
  • 关键成功因素法(CSF)深度解析:从战略目标到数据字典
  • 后训练(Post-training)语言模型
  • NuGet02-包制作及管理
  • 本地部署Nacos开源服务平台,并简单操作实现外部访问,Windows 版本
  • Oracle数据库索引性能机制深度解析:从数据结构到企业实践的系统性知识体系