第18课:Python|异常处理【try-except-finally、自定义异常与程序容错机制】

在这里插入图片描述


📖 开篇导读

前几节课我们学习了文件操作,代码越来越复杂,程序在任何环节都可能出错:用户输入了非数字、文件不存在、网络连接断开、除数为零、内存不足……这些错误在Python中被称为异常。如果不处理异常,程序就会直接崩溃,并输出一大串红色的错误信息,这对用户来说是无法接受的。

优秀的程序不仅要实现功能,还要具备容错性——能够优雅地处理异常,给出有用的提示,然后继续运行或安全退出。异常处理正是实现这一目标的关键技术。

💡 工作场景:后端接口需要捕获数据库查询异常并返回友好提示;文件处理时要处理文件不存在的异常;用户输入验证要捕获类型转换错误;网络请求要处理超时和连接异常。异常处理让程序更健壮,也更容易调试。

本课我们将学习:

  • 异常的概念和常见异常类型
  • try-except-else-finally结构
  • 捕获多个异常与异常链
  • 抛出异常(raise)与自定义异常
  • assert断言的使用
  • 最佳实践与性能注意事项

学完本课,你将能够编写出防御性强、用户体验好的健壮程序。


🎯 学习目标

目标编号具体掌握内容对应面试/工作价值
1️⃣理解异常的概念和常见异常类型读懂错误信息,快速定位问题
2️⃣掌握try-except捕获异常的基本用法防止程序因异常崩溃
3️⃣熟练使用elsefinally子句资源释放、异常后处理
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:,因为它会捕获KeyboardInterruptSystemExit等,导致无法正常退出程序。通常捕获Exception或其子类。

2.4 获取异常对象

使用as关键字可以获取异常实例,获取详细信息。

try:
    f = open("missing.txt")
except FileNotFoundError as e:
    print(f"错误: {e}")
    print(f"错误号: {e.errno}")
    print(f"文件名: {e.filename}")

三、elsefinally 子句

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之前执行。即使tryexcept中使用了returnfinally仍会执行后再返回。

def test():
    try:
        return 1
    finally:
        print("finally执行了")
    # 返回1,但先打印"finally执行了"

3.3 完整的try-except-else-finally

执行顺序:

  1. 执行try
  2. 如果发生异常,跳过try剩余部分,匹配except
  3. 如果没有异常,执行else
  4. 无论是否异常,最后执行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(优化)标志禁用断言。
  • 不要依赖断言做业务逻辑验证,因为它们可以被关闭。
  • 适用于开发阶段测试不变条件。

七、异常处理的最佳实践

  1. 只捕获你能够处理的异常:无法处理的异常应该让它向上传播。
  2. 在合适层级捕获:底层函数往往不捕获,由上层统一处理。
  3. 尽量精确捕获异常:不要使用裸露的except:或捕获BaseException
  4. 使用finally释放资源:但更好的方式是使用with上下文管理器。
  5. 不要忽略异常:空except会隐藏错误,至少记录日志。
  6. 抛出有意义的异常:提供清晰的错误信息。
  7. 使用自定义异常区分业务错误

💻 代码案例实操

案例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:elsefinally演示——数据库模拟

"""
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:会捕获KeyboardInterruptSystemExit等,导致Ctrl+C无法退出使用except Exception:或捕获具体异常
2捕获异常后不做任何处理错误被隐藏,难以调试至少记录日志或打印信息
3finallyreturn会覆盖tryexcept中的return值,导致意外结果不要在finally中使用return
4没有遵循try块只包含可能抛异常的代码捕获范围过大,可能掩盖无关错误仅把可能出错的代码放在try
5raise不带参数用在except块外引发RuntimeError只有活跃异常时才能无参raise
6依赖断言做业务逻辑验证断言可被优化开关禁用,生产环境失效使用if判断并抛出适当的异常
7捕获异常后重新抛出时丢失原始调用栈调试困难使用raise不带参数,或raise ... from e
8自定义异常未继承Exception不会被通用except Exception捕获继承Exception或其子类
9except块中访问可能未定义的变量NameError确保变量在try块中已定义,或在except中定义默认值
10嵌套try-except过多代码难以阅读将异常处理封装到函数中,使用with管理资源

📝 课后实战练习题

第1题:基础异常捕获

编写一个程序,要求用户输入两个整数,计算它们的除法结果。处理以下异常:

  • 输入非数字时提示重新输入
  • 除数为0时提示不能为0
  • 使用循环直到用户输入正确或输入’quit’退出

第2题:文件读取与异常

编写函数read_number_from_file(filename, line_number),读取文本文件中指定行的内容,并将其转换为整数返回。需要处理:

  • 文件不存在
  • 行号超出范围
  • 该行内容不是有效整数
    分别针对不同异常返回不同的错误码或抛出不同异常。

第3题:elsefinally实践

模拟网络请求:编写函数fetch_https://blog.csdn.net/url(url),可能抛出ConnectionErrorTimeoutError。在调用处使用try-except-else-finally,无论是否成功都打印“请求结束”,成功时打印响应数据长度。

第4题:自定义异常

设计一个简单的用户注册系统,要求用户名长度在3-10之间,密码长度至少6位。如果不符合条件,抛出自定义的ValidationError异常(包含具体原因)。在主程序中捕获并打印友好提示。

第5题:异常链

编写三个函数:func1调用func2func2调用func3func3可能抛出ValueError。在func1中捕获ValueError并将其转换为自定义异常AppError,保留原始异常信息(使用from)。测试并打印异常链。

第6题:断言与调试

写一个函数sort_and_remove_duplicates(lst),使用断言确保传入的是列表,且元素是可哈希的。然后实现功能并返回新列表。演示当传入无效参数时断言失败。

第7题:综合——健壮的配置管理器

实现一个配置管理器类ConfigManager,支持从JSON文件加载和保存。提供get(key, default)set(key, value)方法。内部使用异常处理处理文件IO错误、JSON解析错误。如果配置文件损坏,则备份损坏文件并创建新的配置文件。编写测试用例模拟各种错误情况。


🧠 知识点思维导图总结

第18课:异常处理

异常概念

运行时错误

Traceback信息

常见异常类型

try-except

基本语法

捕获多个异常

捕获所有异常(谨慎)

as 获取异常对象

else子句

无异常时执行

依赖try成功

finally子句

总是执行

资源清理

覆盖return的坑

抛出异常

raise语句

重新抛出

异常链 from

自定义异常

继承Exception

添加属性/构造器

业务语义

assert断言

调试检查

可被-O禁用

不要用于业务逻辑

最佳实践

精确捕获

不合适时传播

finally释放资源

不要忽略异常

面试考点

try-except-else-finally顺序

raise vs assert

自定义异常实现

异常与日志结合


🔜 下节课预告

异常处理让程序更健壮,而实际开发中经常需要处理日期时间、随机数等通用功能。Python提供了丰富的内置模块,下一节课我们将学习三个常用模块。

第19课:时间模块、随机模块、系统模块常用功能零基础实战

内容包括:

  • datetime模块:日期时间运算、格式化、解析
  • time模块:时间戳、睡眠、性能计时
  • random模块:随机整数、随机选择、洗牌、随机种子
  • sys模块:命令行参数、标准输入输出、递归深度
  • 实战:生成随机验证码、计时器装饰器、进度条模拟

这些模块是编写实用工具必不可少的,学完你的工具箱会更加完善。

🌟 学习鼓励:异常处理看似“防御性编程”,但它是专业程序员和业余爱好者的重要分水岭。你可能觉得“我的代码不会出错”,但用户和环境总会出乎意料。从现在开始,在编写可能出错的地方都考虑异常处理,你会写出更可靠的程序。加油!


🔗《50节课 Python 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值