【第7章 I/O编程与异常】Python 异常处理全面详解(附丰富实例)

Python 异常处理全面详解(附丰富实例)

异常是程序运行时出现的意外错误(如除数为零、文件不存在等),若不处理会导致程序崩溃。Python 提供了完善的异常处理机制,允许开发者捕获并处理这些错误,保证程序的健壮性。本文将从异常基础、处理流程、自定义异常、实战技巧四个维度,结合 30+ 实例详解 Python 异常处理的方方面面。

一、异常的基本概念

1.1 什么是异常?

异常是程序执行过程中发生的非预期事件,会中断程序的正常流程。例如:

  • 试图除以零(ZeroDivisionError
  • 访问不存在的变量(NameError
  • 打开不存在的文件(FileNotFoundError

示例 1:未处理的异常导致程序崩溃

# 除数为零,触发 ZeroDivisionError
print(10 / 0)

# 程序会直接崩溃,输出错误信息:
# ZeroDivisionError: division by zero

1.2 异常的表现形式

当异常发生时,Python 会:

  1. 生成一个异常对象(包含错误类型、描述、发生位置);
  2. 若未捕获,程序终止并打印回溯信息(Traceback),显示异常类型和发生位置。

示例 2:异常的回溯信息解析

def divide(a, b):
    return a / b

# 调用函数时触发异常
divide(10, 0)

输出的回溯信息

Traceback (most recent call last):
  File "test.py", line 5, in <module>
    divide(10, 0)
  File "test.py", line 2, in divide
    return a / b
ZeroDivisionError: division by zero  # 异常类型和描述
  • 从下往上看:最底部是异常类型ZeroDivisionError)和描述;
  • 上方是调用栈:显示异常发生在 divide 函数的第 2 行,由第 5 行的调用触发。

1.3 常见内置异常类型

Python 内置了 60+ 种异常类型,覆盖各种错误场景,以下是最常用的几种:

异常类型 触发场景 示例
NameError 访问未定义的变量 print(x)(x 未定义)
TypeError 操作或函数应用于错误类型的对象 "2" + 2(字符串与整数相加)
ValueError 函数接收到的参数值合法但不符合要求 int("abc")(字符串无法转为整数)
ZeroDivisionError 除数为零 10 / 0
IndexError 序列索引超出范围 [1,2,3][5]
KeyError 字典中查找不存在的键 {"a":1}["b"]
FileNotFoundError 打开不存在的文件 open("nonexist.txt")
PermissionError 没有操作文件/目录的权限 open("/root/file.txt")(无权限)
UnicodeDecodeError 文本解码失败(如用 UTF-8 解码 GBK 文件) open("gbk.txt", encoding="utf-8").read()
AttributeError 访问对象不存在的属性 "abc".foo()(字符串无 foo 方法)

二、异常处理的核心语法

Python 用 try-except 语句捕获并处理异常,基本结构如下:

try:
    # 可能触发异常的代码块
    risky_operation()
except 异常类型1:
    # 处理异常类型1的代码
except 异常类型2 as e:
    # 处理异常类型2,e 是异常对象(包含错误信息)
else:
    # 无异常时执行的代码(可选)
finally:
    # 无论是否有异常都执行的代码(可选,如资源清理)

2.1 基础用法:try-except 捕获指定异常

示例 3:捕获并处理 ZeroDivisionError

try:
    result = 10 / 0
    print("计算结果:", result)  # 异常发生后,此行不会执行
except ZeroDivisionError:
    print("错误:除数不能为零!")

# 输出:错误:除数不能为零!
# 程序继续执行,不会崩溃

2.2 获取异常信息:as e 捕获异常对象

通过 as e 可获取异常对象,提取错误详情(如 e.argsstr(e))。

示例 4:获取异常详情

try:
    int("abc")
except ValueError as e:
    print("异常类型:", type(e))  # 输出:<class 'ValueError'>
    print("异常描述:", e)        # 输出:invalid literal for int() with base 10: 'abc'
    print("异常参数:", e.args)   # 输出:("invalid literal for int() with base 10: 'abc'",)

2.3 捕获多个异常

可在一个 try 后接多个 except,处理不同类型的异常。

示例 5:处理多种可能的异常

def safe_convert(s):
    try:
        return int(s)  # 尝试转为整数
    except ValueError:
        print(f"错误:'{s}' 无法转为整数")
    except TypeError:
        print(f"错误:类型错误(需字符串,实际是 {type(s)})")

safe_convert("123")    # 正常转换,返回 123
safe_convert("abc")    # 触发 ValueError:错误:'abc' 无法转为整数
safe_convert(123)      # 触发 TypeError:错误:类型错误(需字符串,实际是 <class 'int'>)

2.4 捕获所有异常(不推荐)

except Exception 可捕获所有非系统退出类异常(避免用 except: 捕获包括 KeyboardInterrupt 在内的所有异常)。

示例 6:捕获所有异常(谨慎使用)

try:
    # 可能触发任何异常的代码
    x = 10 / 0
    y = undefined_variable  # 未定义变量
except Exception as e:
    print(f"发生未知错误:{e}")

# 输出:发生未知错误:division by zero(先触发的异常被捕获)

为什么不推荐?
会隐藏代码中的逻辑错误(如拼写错误),难以调试。应优先捕获具体异常类型

2.5 else 子句:无异常时执行

else 块在 try 块无异常时执行,用于分离“正常逻辑”和“异常处理逻辑”。

示例 7:else 子句的用法

try:
    num = int(input("请输入一个整数:"))
except ValueError:
    print("输入错误:请输入有效的整数!")
else:
    # 无异常时执行(输入有效)
    print(f"你输入的整数是:{num},平方是:{num **2}")

# 测试1:输入 "10" → 输出:你输入的整数是:10,平方是:100
# 测试2:输入 "abc" → 输出:输入错误:请输入有效的整数!

2.6 finally 子句:必执行的清理操作

finally 块无论是否有异常都会执行,常用于释放资源(如关闭文件、网络连接)。

示例 8:finally 确保文件关闭

file = None
try:
    file = open("test.txt", "r")
    content = file.read()
    print("文件内容:", content)
except FileNotFoundError:
    print("错误:文件不存在")
finally:
    # 无论是否异常,都确保关闭文件
    if file:
        file.close()
        print("文件已关闭")

# 测试1:文件存在 → 输出文件内容 + "文件已关闭"
# 测试2:文件不存在 → 输出"错误:文件不存在" + "文件已关闭"

更简洁的方式:用 with 语句自动管理资源(替代 finally 关闭文件):

try:
    with open("test.txt", "r") as file:  # with 语句自动关闭文件
        content = file.read()
        print("文件内容:", content)
except FileNotFoundError:
    print("错误:文件不存在")

三、异常的主动触发与传递

3.1 主动触发异常:raise 语句

raise 可手动触发异常,用于验证输入合法性或标记未实现的功能。

示例 9:主动触发异常

def register(username):
    if len(username) < 3:
        # 主动触发 ValueError
        raise ValueError(f"用户名 '{username}' 太短,至少需要3个字符")
    print(f"用户名 '{username}' 注册成功")

try:
    register("ab")  # 长度不足,触发异常
except ValueError as e:
    print("注册失败:", e)

# 输出:注册失败:用户名 'ab' 太短,至少需要3个字符

3.2 重新抛出异常:raise 不带参数

捕获异常后,若无法处理,可通过 raise 重新抛出,让上层代码处理。

示例 10:重新抛出异常

def read_data(filename):
    try:
        with open(filename, "r") as f:
            return f.read()
    except FileNotFoundError as e:
        print(f"read_data 函数捕获到错误:{e}")
        raise  # 重新抛出异常,让调用者处理

try:
    data = read_data("nonexist.txt")
except FileNotFoundError as e:
    print(f"主程序处理错误:{e}")

# 输出:
# read_data 函数捕获到错误:[Errno 2] No such file or directory: 'nonexist.txt'
# 主程序处理错误:[Errno 2] No such file or directory: 'nonexist.txt'

3.3 异常的传递性

若函数内发生异常且未捕获,异常会向上传递到调用者,直至被捕获或导致程序崩溃。

示例 11:异常的传递

def level3():
    print("进入 level3")
    1 / 0  # 触发异常,未处理
    print("离开 level3")  # 不会执行

def level2():
    print("进入 level2")
    level3()  # 调用 level3,异常传递至此
    print("离开 level2")  # 不会执行

def level1():
    print("进入 level1")
    level2()  # 调用 level2,异常传递至此
    print("离开 level1")  # 不会执行

# 主程序调用 level1
try:
    level1()
except ZeroDivisionError:
    print("主程序捕获到异常")

# 输出:
# 进入 level1
# 进入 level2
# 进入 level3
# 主程序捕获到异常

四、自定义异常

当内置异常无法满足需求时,可通过继承 Exception定义自定义异常,使错误类型更具可读性。

4.1 自定义异常的基本定义

# 定义自定义异常(继承 Exception)
class InvalidAgeError(Exception):
    """当年龄不合法时触发的异常"""
    # 可自定义初始化方法,添加更多信息
    def __init__(self, age, message="年龄必须在 0-120 之间"):
        self.age = age
        self.message = message
        super().__init__(self.message)  # 调用父类构造函数

    # 可选:自定义异常描述
    def __str__(self):
        return f"{self.message},实际输入:{self.age}"

4.2 使用自定义异常

示例 12:触发和处理自定义异常

def check_age(age):
    if not (0 <= age <= 120):
        # 触发自定义异常
        raise InvalidAgeError(age)
    print(f"年龄 {age} 合法")

try:
    check_age(150)  # 超出范围,触发异常
except InvalidAgeError as e:
    print("检查失败:", e)  # 输出:检查失败:年龄必须在 0-120 之间,实际输入:150

try:
    check_age(-5)
except InvalidAgeError as e:
    print("检查失败:", e)  # 输出:检查失败:年龄必须在 0-120 之间,实际输入:-5

4.3 自定义异常的继承关系

可通过继承自定义异常,创建更细分的异常类型。

示例 13:异常的继承体系

# 基础异常
class ValidationError(Exception):
    pass

# 细分异常(继承自 ValidationError)
class TooSmallError(ValidationError):
    pass

class TooLargeError(ValidationError):
    pass

def validate_number(n):
    if n < 0:
        raise TooSmallError(f"{n} 不能小于 0")
    if n > 100:
        raise TooLargeError(f"{n} 不能大于 100")
    print(f"{n} 验证通过")

try:
    validate_number(-5)
except TooSmallError as e:
    print("处理过小错误:", e)
except TooLargeError as e:
    print("处理过大错误:", e)
except ValidationError as e:  # 捕获所有子类异常
    print("处理其他验证错误:", e)

# 输出:处理过小错误:-5 不能小于 0

五、异常处理的实战场景

场景 1:文件操作的异常处理

文件操作可能触发多种异常(文件不存在、权限不足、编码错误等),需针对性处理。

示例 14:安全读取文件

def safe_read_file(filename):
    try:
        with open(filename, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        return f"错误:文件 '{filename}' 不存在"
    except PermissionError:
        return f"错误:没有权限读取 '{filename}'"
    except UnicodeDecodeError:
        return f"错误:'{filename}' 编码不是 UTF-8,无法解码"
    except Exception as e:  # 捕获其他未知错误
        return f"读取文件时发生未知错误:{e}"

# 测试不同场景
print(safe_read_file("nonexist.txt"))  # 输出:错误:文件 'nonexist.txt' 不存在
print(safe_read_file("/root/secret.txt"))  # 输出:错误:没有权限读取 '/root/secret.txt'(假设无权限)

场景 2:网络请求的异常处理

网络请求可能因超时、连接失败等触发异常,需重试或友好提示。

示例 15:带重试机制的网络请求

import requests
from requests.exceptions import RequestException, ConnectionError, Timeout

def fetch_url(url, max_retries=3):
    for retry in range(max_retries):
        try:
            response = requests.get(url, timeout=5)
            response.raise_for_status()  # 触发 HTTP 错误(如 404、500)
            return response.text
        except ConnectionError:
            print(f"连接失败,正在重试({retry+1}/{max_retries})...")
        except Timeout:
            print(f"请求超时,正在重试({retry+1}/{max_retries})...")
        except RequestException as e:
            print(f"请求错误:{e},不再重试")
            break
    return None  # 多次重试失败

# 测试(需联网)
content = fetch_url("https://www.baidu.com")
if content:
    print("请求成功,内容长度:", len(content))
else:
    print("请求失败")

场景 3:用户输入验证

处理用户输入时,需验证合法性并返回友好错误提示。

示例 16:命令行计算器(输入验证)

def calculator():
    while True:
        try:
            # 获取用户输入
            num1 = float(input("请输入第一个数字(输入 q 退出):"))
            op = input("请输入运算符(+、-、*、/):")
            num2 = float(input("请输入第二个数字:"))

            # 计算并输出结果
            if op == "+":
                print(f"结果:{num1 + num2}")
            elif op == "-":
                print(f"结果:{num1 - num2}")
            elif op == "*":
                print(f"结果:{num1 * num2}")
            elif op == "/":
                if num2 == 0:
                    raise ZeroDivisionError("除数不能为零")
                print(f"结果:{num1 / num2}")
            else:
                raise ValueError(f"不支持的运算符:{op}")
        except ValueError as e:
            if str(e).startswith("could not convert string to float"):
                # 用户输入非数字,检查是否要退出
                if input.strip().lower() == "q":
                    print("退出计算器")
                    break
                print("错误:请输入有效的数字!")
            else:
                print("错误:", e)
        except ZeroDivisionError as e:
            print("错误:", e)
        print("-" * 30)

calculator()

六、异常处理的最佳实践

6.1 捕获具体异常,而非通用异常

# 错误:捕获所有异常,隐藏潜在问题
try:
    x = 10 / 0
except:  # 不推荐!会捕获 KeyboardInterrupt 等系统异常
    print("出错了")

# 正确:捕获具体异常
try:
    x = 10 / 0
except ZeroDivisionError:
    print("除数不能为零")

6.2 避免空 except

except 块会吞噬异常,导致问题难以排查:

# 错误:空 except 块,无法知道发生了什么
try:
    risky_operation()
except:
    pass  # 什么都不做,隐藏错误

# 正确:至少记录异常信息
import logging
try:
    risky_operation()
except Exception as e:
    logging.error(f"发生错误:{e}", exc_info=True)  # 记录异常详情

6.3 释放资源用 finally 或上下文管理器

处理文件、数据库连接等资源时,确保释放:

# 推荐:用 with 语句(上下文管理器)自动释放
with open("file.txt", "r") as f:
    content = f.read()

# 等价于:
f = open("file.txt", "r")
try:
    content = f.read()
finally:
    f.close()

6.4 自定义异常提升代码可读性

用自定义异常区分业务错误,使代码更易维护:

# 不推荐:用内置异常,语义模糊
def withdraw(amount):
    if amount > balance:
        raise ValueError("余额不足")

# 推荐:自定义异常,语义清晰
class InsufficientFundsError(Exception):
    pass

def withdraw(amount):
    if amount > balance:
        raise InsufficientFundsError("余额不足,无法取款")

6.5 异常信息应明确具体

错误信息需包含足够细节,便于调试:

# 差:信息模糊
try:
    int(input_str)
except ValueError:
    print("输入错误")

# 好:信息具体
try:
    int(input_str)
except ValueError:
    print(f"输入 '{input_str}' 无法转换为整数,请输入数字")

七、总结

Python 异常处理是保证程序健壮性的核心机制,关键知识点包括:
1.** 异常基础 :异常是运行时错误,会中断程序,需用 try-except 捕获;
2.
核心语法 try( risky 代码)、except(处理异常)、else(无异常执行)、finally(必执行);
3.
主动控制 raise 触发异常,异常可向上传递;
4.
自定义异常 :继承 Exception,增强错误类型的可读性;
5.
最佳实践**:捕获具体异常、释放资源、提供清晰错误信息。

掌握异常处理,能写出更健壮、易维护的代码,尤其是在文件操作、网络请求、用户输入等易出错的场景中至关重要。

Python 异常处理结构的全面性与扩展性分析

Python 中 try-except-else-finally 的基础结构(如下)是异常处理的核心框架,但其全面性和通用性需结合具体场景分析。本文将从结构完整性、适用场景、扩展需求三个维度,探讨该结构的局限性及补充方案。

try:
    # 可能触发异常的代码块
    risky_operation()
except 异常类型1:
    # 处理异常类型1的代码
except 异常类型2 as e:
    # 处理异常类型2,e 是异常对象
else:
    # 无异常时执行的代码(可选)
finally:
    # 无论是否有异常都执行的代码(可选)

一、基础结构的全面性:覆盖核心场景,但非“万能”

1. 优势:覆盖异常处理的核心需求

该结构通过四个子句的组合,已能满足80%的常规异常处理场景

  • try:定位风险代码,是异常捕获的起点;
  • except:针对性处理已知异常,避免程序崩溃;
  • else:分离“正常逻辑”与“异常逻辑”,使代码更清晰;
  • finally:确保资源释放(如关闭文件、连接),保障程序健壮性。

例如,文件操作中常见的“读取-处理-关闭”流程,用该结构可完美覆盖:

try:
    f = open("data.txt", "r")
    content = f.read()
except FileNotFoundError:
    print("文件不存在")
else:
    print(f"处理内容:{content[:100]}")  # 无异常时才处理
finally:
    if 'f' in locals():  # 确保文件对象存在
        f.close()
        print("文件已关闭")

2. 局限性:未覆盖的特殊场景

尽管基础结构适用广泛,但在以下场景中存在不足:

(1)异常类型的精细划分不足

基础结构中 except 子句需显式指定异常类型,但实际开发中可能遇到:
-** 异常继承关系 **:若父类异常在子类前捕获,会导致子类异常无法被针对性处理。

# 错误示例:Exception 是所有异常的父类,会拦截子类异常
try:
    1 / 0
except Exception:
    print("捕获到异常")
except ZeroDivisionError:  # 此句永远不会执行
    print("除数为零")

-** 多异常合并处理 **:当多个异常需要相同处理逻辑时,基础结构需重复代码(可通过元组优化,但未在基础结构中体现)。

(2)缺乏异常的“重试”机制

在网络请求、文件读写等场景中,异常可能是暂时性的(如网络波动),基础结构未提供重试能力。例如:

# 基础结构无法直接实现重试,需额外嵌套循环
def fetch_data(url, retries=3):
    for _ in range(retries):
        try:
            return requests.get(url)
        except ConnectionError:
            print("连接失败,重试...")
    raise  # 重试次数耗尽后抛出异常

(3)未涉及异常与日志的结合

基础结构仅处理异常本身,未包含日志记录(调试关键)。实际开发中,需手动添加日志:

import logging
try:
    risky_operation()
except Exception as e:
    logging.error(f"发生错误:{e}", exc_info=True)  # 记录异常详情
    raise  # 重新抛出

(4)未覆盖“异常链”场景

当一个异常触发另一个异常时(如处理异常时又出错),基础结构无法追踪完整异常链。例如:

try:
    1 / 0
except ZeroDivisionError as e:
    # 处理时触发新异常,但基础结构无法关联原始异常
    raise ValueError("处理除法时出错") from e  # 需要显式用 from 关联

二、通用性分析:适用常规场景,复杂场景需扩展

1. 通用场景的适配性

对于单层级、已知异常类型的场景(如文件读写、参数验证),基础结构足够通用:

  • 逻辑简单:异常类型明确(如 FileNotFoundErrorValueError);
  • 处理单一:无需复杂的重试、日志或异常链追踪。

例如,用户输入验证:

try:
    age = int(input("请输入年龄:"))
except ValueError:
    print("年龄必须是数字")
else:
    print(f"您的年龄是:{age}")

2. 复杂场景的扩展需求

多层级调用、分布式系统、高可靠性要求的场景中,基础结构需结合额外机制:

(1)多层级异常处理

当异常在函数调用链中传递时,需在不同层级做不同处理(如底层记录日志,上层返回友好提示):

# 底层函数:记录异常详情
def read_config():
    try:
        return open("config.ini").read()
    except Exception as e:
        logging.error(f"读取配置失败:{e}", exc_info=True)  # 底层记录日志
        raise  # 向上传递

# 上层函数:返回用户友好提示
def load_app():
    try:
        read_config()
    except Exception:
        print("应用启动失败,请检查配置文件")  # 上层处理用户交互

(2)上下文管理器与异常

with 语句(上下文管理器)本质是 try-finally 的语法糖,但可简化资源管理,是基础结构的有益补充:

# 用上下文管理器替代手动 finally 关闭文件
with open("data.txt", "r") as f:  # 自动调用 f.close()
    content = f.read()

其内部逻辑等价于基础结构的 try-finally,但更简洁。

(3)自定义异常体系

当内置异常无法区分业务错误时,需扩展基础结构以支持自定义异常:

class InvalidUserError(Exception):
    pass

try:
    if user not in valid_users:
        raise InvalidUserError(f"用户 {user} 不存在")
except InvalidUserError as e:
    print(f"业务错误:{e}")

三、可补充的扩展方案

1. 异常分组处理(合并同类异常)

用元组在 except 中同时捕获多个异常,减少代码重复:

try:
    data = int(input("请输入数字:"))
except (ValueError, TypeError) as e:  # 同时处理两种异常
    print(f"输入错误:{e}")

2. 异常链追踪(保留原始异常上下文)

raise ... from 关联异常链,便于调试:

try:
    config = open("config.ini").read()
except FileNotFoundError as e:
    # 新异常关联原始异常,形成异常链
    raise RuntimeError("无法启动应用") from e

输出的回溯信息会同时显示两个异常,清晰展示错误根源。

3. 重试装饰器(封装重试逻辑)

用装饰器抽象重试逻辑,扩展基础结构的能力:

from functools import wraps

def retry(max_retries=3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"重试 {i+1}/{max_retries}:{e}")
            raise  # 重试耗尽后抛出
        return wrapper
    return decorator

# 使用装饰器为函数添加重试能力
@retry(max_retries=2)
def fetch(url):
    return requests.get(url)

4. 日志集成(自动记录异常)

结合日志模块,确保异常被记录:

import logging
logging.basicConfig(filename="app.log", level=logging.ERROR)

try:
    1 / 0
except ZeroDivisionError as e:
    logging.error("除法错误", exc_info=True)  # 记录完整异常信息
    raise

5. 异常的类型判断与转换

在复杂系统中,可能需要将底层异常转换为上层可理解的异常:

try:
    db.query("SELECT * FROM users")
except DatabaseError as e:
    # 将数据库异常转换为业务异常
    if "connection refused" in str(e):
        raise ConnectionError("数据库连接失败") from e
    else:
        raise DataAccessError("数据查询失败") from e

四、结论:基础结构是核心,扩展需结合场景

try-except-else-finally 结构是 Python 异常处理的基础核心,其设计已覆盖“捕获-处理-清理”的核心流程,足以应对大多数常规场景(如文件操作、参数验证、简单函数调用)。

但在复杂系统(如分布式服务、高可靠应用)中,需通过以下方式扩展:

  1. 异常分组与继承:精细处理不同类型的异常;
  2. 异常链与日志:追踪错误根源,便于调试;
  3. 重试与装饰器:应对暂时性错误;
  4. 自定义异常:区分业务逻辑错误;
  5. 上下文管理器:简化资源释放。

简言之,基础结构是“骨架”,而扩展机制是“血肉”——开发者需根据场景灵活组合,才能构建健壮且易维护的异常处理体系。

posted @ 2025-11-15 22:30  wangya216  阅读(247)  评论(0)    收藏  举报