第十六节:异常处理:让程序在报错中稳定运行

上一篇咱们掌握了文件操作与数据持久化的核心技能,通过文本、JSON文件读写,让学生信息管理系统具备了长期存储数据的"记忆"能力,还优化了系统的实用性与易用性。但在实际运行中,程序难免会遇到意外:读取的文件被误删、用户输入了非法数据、权限不足无法写入文件等,这些场景都会导致程序抛出错误并直接崩溃,既影响用户体验,还可能造成数据丢失。想要让程序具备"容错能力",在异常场景下也能优雅运行、合理提示,就必须掌握异常处理机制------这是构建健壮程序的核心环节。

今天咱们聚焦思维导图"异常处理"核心模块,从异常的本质与分类、try-except核心语法、else-finally补充机制,到主动抛异常、自定义异常,再到实战中优化学生信息管理系统,逐一深入拆解。学会这些内容,你将能精准捕获并处理程序运行中的各类报错,让程序从"一错就崩"升级为"遇错不慌",同时为下一章"模块与包"(实现代码模块化、可复用)打下稳定基础。

一、异常基础:理解程序报错的本质

在Python中,异常是程序运行时发生的错误(如语法正确但逻辑异常),当异常发生且未被处理时,Python解释器会打印异常信息(Traceback),并终止程序执行。异常并非洪水猛兽,它是程序对"意外场景"的提醒,而异常处理就是告诉程序"遇到这类意外该怎么做"。

1. 异常与语法错误的区别

很多初学者会混淆"异常"与"语法错误",二者本质不同,处理方式也完全不一样:

  • 语法错误(SyntaxError):代码编写不符合Python语法规则,程序在执行前就会报错,无法启动。例如少写冒号、缩进错误、引号不闭合等,属于"编写阶段错误",必须修正代码才能运行。

  • 异常(Exception):代码语法正确,程序可正常启动,但运行中遇到意外场景导致报错。例如读取不存在的文件、除数为0、字符串转数字失败等,属于"运行阶段错误",可通过异常处理机制捕获并处理。

python 复制代码
# 示例1:语法错误(缩进错误),程序无法启动
def test():
print("语法错误")  # 报错:IndentationError: expected an indented block

# 示例2:异常(运行时错误),程序启动后报错
def divide(a, b):
    return a / b

divide(10, 0)  # 运行时报错:ZeroDivisionError: division by zero

2. Python常见内置异常(实战高频)

Python内置了大量异常类,覆盖各类运行时错误场景,以下是实战中最常遇到的10种异常,需牢记其含义与触发场景,以便精准捕获:

异常类型 核心含义 触发场景示例
FileNotFoundError 文件不存在 open("不存在的文件.txt", 'r')
ZeroDivisionError 除数为0 10 / 0、5 // 0
ValueError 值错误(类型正确但值非法) int("abc")、float("xyz")
TypeError 类型错误(操作对象类型不匹配) "10" + 5、len(123)
KeyError 字典键不存在 student = {"name":"小明"}; student["age"]
IndexError 列表/元组索引越界 lst = [1,2,3]; lst[5]
PermissionError 权限不足 写入受保护的系统文件、删除无权限的文件
UnicodeDecodeError 编码解码错误 用gbk编码读取utf-8格式的文件
JSONDecodeError JSON格式非法 json.load()读取格式错误的JSON文件
AttributeError 对象属性/方法不存在 student = Student(); student.grade(无grade属性)

补充:所有内置异常都继承自BaseException类,其中Exception是最常用的异常父类(除SystemExit、KeyboardInterrupt等退出类异常外,多数异常都继承自它),后续自定义异常也需继承此类。

二、异常处理核心语法:try-except语句

异常处理的核心是`try-except`语句,其逻辑是:将可能抛出异常的代码放入`try`块,当`try`块中代码正常运行时,跳过`except`块;当`try`块中抛出异常时,终止`try`块执行,转而执行对应异常类型的`except`块,从而避免程序崩溃。

1. 基础语法:捕获指定异常

针对已知可能发生的异常,精准捕获对应类型,既能处理异常,又能避免捕获无关错误,便于定位问题。

python 复制代码
# 语法格式
try:
    # 可能抛出异常的代码块
    代码逻辑
except 异常类型1:
    # 捕获到异常类型1时执行的处理逻辑
    异常处理代码
except 异常类型2:
    # 捕获到异常类型2时执行的处理逻辑
    异常处理代码

# 示例:处理文件读取的FileNotFoundError和UnicodeDecodeError
try:
    with open("students.json", 'r', encoding='utf-8') as f:
        content = f.read()
    print("文件读取成功")
except FileNotFoundError:
    print("异常处理:文件不存在,请检查文件路径是否正确")
except UnicodeDecodeError:
    print("异常处理:编码错误,请确认文件编码格式为utf-8")

核心优势:精准捕获目标异常,每个异常对应专属处理逻辑,代码可读性与可维护性更强。

2. 多异常合并捕获

当多个异常的处理逻辑相同时,可将其放入一个`except`块,用元组包裹多个异常类型,简化代码。

python 复制代码
try:
    # 可能触发ValueError或TypeError的代码
    num1 = int(input("请输入第一个数字:"))
    num2 = int(input("请输入第二个数字:"))
    result = num1 / num2
    print(f"计算结果:{result}")
except (ValueError, TypeError):
    # 同时处理"值错误"和"类型错误"
    print("异常处理:请输入合法的整数")
except ZeroDivisionError:
    print("异常处理:除数不能为0")

3. 捕获所有异常(慎用)

若无法预判可能发生的异常类型,可使用`except Exception:`捕获所有非退出类异常(不推荐轻易使用,易掩盖潜在问题),或用`except:`捕获所有异常(包括退出类异常,更不推荐)。

python 复制代码
try:
    num1 = int(input("请输入数字:"))
    num2 = int(input("请输入另一个数字:"))
    print(num1 / num2)
except Exception as e:
    # as e:获取异常详细信息,便于调试
    print(f"程序发生异常:{type(e).__name__},详情:{e}")

注意:捕获所有异常会隐藏代码中的逻辑漏洞(如拼写错误、逻辑错误),仅建议在程序顶层兜底处理,或临时调试时使用,日常开发优先精准捕获指定异常。

4. else语句:无异常时执行

`else`可与`try-except`搭配使用,当`try`块中代码无异常正常执行完毕后,会执行`else`块内容;若发生异常,则跳过`else`块。适用于"异常时处理错误,正常时执行后续逻辑"的场景。

python 复制代码
try:
    num1 = int(input("请输入整数:"))
    num2 = int(input("请输入另一个整数:"))
    result = num1 / num2
except ValueError:
    print("异常处理:请输入合法整数")
except ZeroDivisionError:
    print("异常处理:除数不能为0")
else:
    # 无异常时执行,输出计算结果
    print(f"计算成功,结果为:{result:.2f}")

5. finally语句:无论是否异常都执行

`finally`与`try-except`搭配使用,无论`try`块是否发生异常、`except`块是否执行,`finally`块内容都会强制执行。核心用途是释放资源(如关闭文件、断开数据库连接),即使发生异常也能保障资源正常释放。

python 复制代码
# 示例:手动打开文件时,用finally确保文件关闭
f = None
try:
    f = open("test.txt", 'r', encoding='utf-8')
    content = f.read()
    print("文件读取内容:", content)
except FileNotFoundError:
    print("异常处理:文件不存在")
finally:
    # 无论是否异常,都关闭文件
    if f is not None:
        f.close()
        print("文件已关闭,资源释放完成")

补充:之前学的`with`语句本质上是`try-finally`的语法糖,自动帮我们释放资源,无需手动关闭文件,但`finally`在非文件操作场景(如数据库连接)中仍有广泛应用。

6. 完整组合:try-except-else-finally

将四个语句组合使用,可实现完整的异常处理逻辑:尝试执行代码→异常时处理→无异常时执行后续→无论是否异常都释放资源。

python 复制代码
try:
    num1 = int(input("请输入被除数:"))
    num2 = int(input("请输入除数:"))
    result = num1 / num2
except (ValueError, TypeError):
    print("异常处理:请输入合法整数")
except ZeroDivisionError:
    print("异常处理:除数不能为0")
else:
    print(f"计算结果:{result}")
finally:
    print("程序执行完毕(无论是否异常,此语句都会执行)")

三、主动抛出异常:raise语句

除了捕获程序运行时自动抛出的异常,我们还可通过`raise`语句主动抛出异常,用于对自定义规则的校验(如参数合法性、业务逻辑合规性),明确告知程序"当前场景不符合预期,需触发异常处理"。

1. 基础用法:抛出指定异常

用`raise 异常类型()`主动抛出异常,可添加异常信息(字符串),让异常更易理解。

python 复制代码
def check_age(age):
    # 自定义规则:年龄需在0-120之间,否则主动抛异常
    if not isinstance(age, int):
        raise TypeError("年龄必须是整数类型")
    if age < 0 or age > 120:
        raise ValueError(f"年龄{age}不合法,需在0-120之间")
    print(f"年龄合法:{age}岁")

# 测试主动抛异常
try:
    check_age(150)
except (TypeError, ValueError) as e:
    print(f"异常处理:{e}")  # 输出:异常处理:年龄150不合法,需在0-120之间

try:
    check_age("20")
except (TypeError, ValueError) as e:
    print(f"异常处理:{e}")  # 输出:异常处理:年龄必须是整数类型

2. 重新抛出异常:不掩盖异常

有时需要先捕获异常做一些处理(如记录日志),再将异常重新抛出,让上层代码继续处理。此时用`raise`无参数形式,可保留原异常的类型和详细信息。

python 复制代码
import logging

def read_file(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read()
    except FileNotFoundError as e:
        # 记录异常日志,再重新抛出异常
        logging.error(f"文件读取失败:{e}")
        raise  # 重新抛出原异常,不改变异常信息

# 上层代码处理重新抛出的异常
try:
    content = read_file("不存在的文件.txt")
except FileNotFoundError as e:
    print(f"上层处理:{e}")

四、自定义异常:适配业务场景的专属异常

Python内置异常适用于通用场景,当业务有特殊规则时,可自定义异常类(继承自Exception),让异常更贴合业务逻辑,提升代码可读性和可维护性。例如学生成绩范围校验、用户权限校验等场景,都可定义专属异常。

1. 自定义异常基础语法

自定义异常类需继承Exception(不要继承BaseException,避免捕获退出类异常),可自定义初始化方法,添加专属属性和方法。

python 复制代码
# 自定义异常类(继承Exception)
class ScoreError(Exception):
    """自定义异常:学生成绩不合法(0-100之间)"""
    # 自定义初始化方法,添加异常详情
    def __init__(self, score, message="成绩不合法,需在0-100之间"):
        self.score = score  # 异常关联的成绩值
        self.message = message
        super().__init__(self.message)  # 调用父类初始化方法

    # 自定义异常描述,便于打印
    def __str__(self):
        return f"{self.message},当前成绩:{self.score}"

class PermissionError(Exception):
    """自定义异常:用户权限不足"""
    def __init__(self, user_role, need_role):
        self.user_role = user_role  # 当前用户角色
        self.need_role = need_role  # 所需角色
        super().__init__(f"权限不足:当前角色{user_role},需{need_role}角色")

2. 自定义异常的使用的

自定义异常的使用方法与内置异常一致,用`raise`主动抛出,用`try-except`捕获处理。

python 复制代码
# 测试自定义异常:成绩校验
def check_score(score):
    if not isinstance(score, (int, float)):
        raise TypeError("成绩必须是数字类型")
    if score < 0 or score > 100:
        # 主动抛出自定义异常
        raise ScoreError(score)
    print(f"成绩校验通过:{score}分")

# 测试自定义异常:权限校验
def check_permission(user_role, need_role):
    if user_role != need_role:
        raise PermissionError(user_role, need_role)
    print("权限校验通过,可执行操作")

# 捕获自定义异常
try:
    check_score(105)
except (TypeError, ScoreError) as e:
    print(f"异常处理:{e}")  # 输出:异常处理:成绩不合法,需在0-100之间,当前成绩:105

try:
    check_permission("student", "admin")
except PermissionError as e:
    print(f"异常处理:{e}")  # 输出:异常处理:权限不足:当前角色student,需admin角色

五、实战:给学生信息管理系统添加全面异常处理

结合本章异常处理知识,优化上一章的学生信息管理系统,针对文件操作、用户输入、业务逻辑等场景添加异常处理,解决"文件不存在、JSON格式错误、输入非法数据、权限不足"等问题,让系统在异常场景下也能稳定运行,同时给出友好提示。

python 复制代码
import json
import logging

# 自定义异常类
class ScoreError(Exception):
    """自定义异常:成绩不合法(0-100之间)"""
    def __init__(self, score):
        self.score = score
        super().__init__(f"成绩{score}不合法,需在0-100之间")

class PermissionError(Exception):
    """自定义异常:用户权限不足"""
    def __init__(self, user_role):
        super().__init__(f"权限不足:{user_role}无此操作权限")

# 父类:用户类
class User(object):
    def __init__(self, name, account, password):
        self.name = name
        self.account = account
        self.__password = password
    
    def __verify_pwd(self, input_pwd):
        return self.__password == input_pwd
    
    def login(self, input_pwd):
        try:
            if not isinstance(input_pwd, str):
                raise TypeError("密码必须是字符串类型")
            if self.__verify_pwd(input_pwd):
                print(f"{self.name}({self.account})登录成功")
                return True
            else:
                raise ValueError("密码错误")
        except TypeError as e:
            print(f"登录异常:{e}")
            return False
        except ValueError as e:
            print(f"登录异常:{e}")
            return False

# 子类:学生类
class Student(User):
    def __init__(self, name, account, password, score):
        super().__init__(name, account, password)
        self.score = score
    
    def check_score(self):
        print(f"{self.name}的成绩:{self.score}分")
    
    def to_dict(self):
        return {
            "name": self.name,
            "account": self.account,
            "password": self._User__password,
            "score": self.score
        }

# 子类:管理员类
class Admin(User):
    def __init__(self, name, account, password):
        super().__init__(name, account, password)
    
    def manage_student(self, manager, action, *args):
        try:
            if action == "add":
                name, age, score = args
                # 校验成绩合法性
                if not isinstance(score, (int, float)):
                    raise TypeError("成绩必须是数字类型")
                if score < 0 or score > 100:
                    raise ScoreError(score)
                manager.add_student(name, age, score)
            elif action == "modify":
                name, new_score = args
                if not isinstance(new_score, (int, float)):
                    raise TypeError("成绩必须是数字类型")
                if new_score < 0 or new_score > 100:
                    raise ScoreError(new_score)
                manager.modify_student(name, new_score=new_score)
            elif action == "delete":
                name = args[0]
                manager.delete_student(name)
            else:
                raise ValueError(f"操作类型{action}不合法,仅支持add/modify/delete")
        except (TypeError, ScoreError, ValueError) as e:
            print(f"管理操作异常:{e}")

# 学生信息管理类(添加全面异常处理)
class StudentManager(object):
    def __init__(self, file_path="students.json"):
        self.file_path = file_path
        self.student_list = self.load_data()
    
    def load_data(self):
        try:
            with open(self.file_path, 'r', encoding='utf-8') as f:
                # 捕获JSON格式错误
                try:
                    data = json.load(f)
                except json.JSONDecodeError as e:
                    logging.error(f"JSON格式错误:{e}")
                    raise ValueError("数据文件格式非法,无法加载") from e
                # 校验数据结构
                if not isinstance(data, list):
                    raise ValueError("数据文件格式错误,需为列表类型")
                student_list = []
                for item in data:
                    if not isinstance(item, dict) or not all(key in item for key in ["name", "account", "password", "score"]):
                        raise ValueError(f"数据项格式错误:{item}")
                    student = Student(
                        name=item["name"],
                        account=item["account"],
                        password=item["password"],
                        score=item["score"]
                    )
                    student_list.append(student)
                print("数据加载成功")
                return student_list
        except FileNotFoundError:
            print("数据文件不存在,初始化空列表")
            return []
        except PermissionError:
            print("权限不足,无法读取数据文件")
            return []
        except UnicodeDecodeError:
            print("编码错误,文件编码需为utf-8")
            return []
        except ValueError as e:
            print(f"数据加载异常:{e}")
            return []
        except Exception as e:
            logging.error(f"未知异常:{e}")
            print("数据加载失败,未知异常")
            return []
    
    def save_data(self):
        try:
            data = [student.to_dict() for student in self.student_list]
            with open(self.file_path, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            print("数据保存成功")
        except PermissionError:
            print("权限不足,无法写入数据文件")
        except Exception as e:
            logging.error(f"数据保存异常:{e}")
            print("数据保存失败,未知异常")
    
    def add_student(self, name, age, score):
        student = Student(
            name=name,
            account=f"stu_{len(self.student_list)+1:03d}",
            password="123456",
            score=score
        )
        self.student_list.append(student)
        self.save_data()
        print(f"成功添加学生:{name},默认账号:{student.account},默认密码:123456")
    
    def query_student(self, name):
        try:
            if not isinstance(name, str):
                raise TypeError("查询姓名必须是字符串类型")
            for student in self.student_list:
                if student.name == name:
                    print(f"查询结果:姓名:{student.name},账号:{student.account},成绩:{student.score}")
                    return student
            print(f"未找到姓名为{name}的学生")
            return None
        except TypeError as e:
            print(f"查询异常:{e}")
            return None
    
    def modify_student(self, name, new_score=None):
        student = self.query_student(name)
        if student and new_score:
            student.score = new_score
            self.save_data()
            print(f"成功修改{name}的成绩,当前成绩:{student.score}分")
    
    def delete_student(self, name):
        try:
            if not isinstance(name, str):
                raise TypeError("删除姓名必须是字符串类型")
            for i, student in enumerate(self.student_list):
                if student.name == name:
                    del self.student_list[i]
                    self.save_data()
                    print(f"成功删除学生:{name}")
                    return
            print(f"未找到姓名为{name}的学生,删除失败")
        except TypeError as e:
            print(f"删除异常:{e}")
    
    def show_all_students(self):
        if not self.student_list:
            print("暂无学生信息")
            return
        print("所有学生信息:")
        for i, student in enumerate(self.student_list, 1):
            print(f"第{i}名:姓名:{student.name},账号:{student.account},成绩:{student.score}")

# 测试优化后的系统
if __name__ == "__main__":
    # 初始化日志配置
    logging.basicConfig(filename="error.log", level=logging.ERROR, format="%(asctime)s - %(levelname)s - %(message)s")
    manager = StudentManager()
    admin = Admin("管理员", "admin", "admin123")
    
    if admin.login("admin123"):
        # 测试异常场景:添加成绩非法的学生
        admin.manage_student(manager, "add", "小明", 18, 105)
        # 测试正常场景:添加合法学生
        admin.manage_student(manager, "add", "小明", 18, 90)
        admin.manage_student(manager, "add", "小红", 17, 88)
        manager.show_all_students()
        
        # 测试异常场景:修改成绩为字符串
        admin.manage_student(manager, "modify", "小红", "优秀")
        # 测试正常场景:修改成绩
        admin.manage_student(manager, "modify", "小红", 92)
        manager.show_all_students()
    
    student = manager.query_student("小红")
    if student:
        student.login("123456")
        student.check_score()

案例分析

  1. 全场景异常覆盖:针对文件操作(读写、编码、权限)、数据校验(成绩、姓名、JSON格式)、用户操作(登录、管理权限)等场景,分别捕获内置异常和自定义异常,避免程序崩溃;

  2. 友好提示与日志记录:异常发生时,既给用户友好的中文提示,又通过logging记录详细异常信息,便于后续调试和问题定位;

  3. 自定义异常适配业务:通过ScoreError、PermissionError等自定义异常,精准匹配学生系统的业务规则,让异常逻辑更清晰;

  4. 代码健壮性提升:即使出现文件损坏、输入非法数据等意外,系统也能正常运行并提示问题,大幅提升用户体验和系统稳定性。

六、异常处理避坑指南

  • 避免过度捕获:不滥用`except Exception:`捕获所有异常,优先精准捕获指定异常,防止掩盖代码逻辑漏洞;

  • 异常信息明确:捕获异常时,给出具体提示(如"文件不存在,请检查路径"而非"文件错误"),便于用户排查问题;

  • 资源释放优先:文件、数据库连接等资源操作,务必用finally或with语句确保资源释放,避免资源泄露;

  • 自定义异常继承正确:自定义异常需继承Exception,不要继承BaseException,避免捕获系统退出等核心异常;

  • 合理记录日志:异常发生时,除了用户提示,还需记录详细日志(时间、异常类型、详情),便于后续调试。

结尾:异常处理搞定了

今天咱们彻底掌握了异常处理的核心内容,从理解异常本质、捕获内置异常,到主动抛异常、自定义业务异常,再到实战中给学生系统添加全场景异常处理,让程序具备了强大的容错能力,实现了"遇错不崩、优雅提示"的目标。现在你已经能开发出健壮、稳定的Python程序,应对各类意外场景。

相关推荐
方安乐2 小时前
杂记:Quart和Flask比较
后端·python·flask
墨雨晨曦882 小时前
leedcode刷题总结
java·开发语言
测试19982 小时前
如何使用Appium实现移动端UI自动化测试?
自动化测试·软件测试·python·测试工具·ui·appium·测试用例
yuankoudaodaokou2 小时前
无图纸如何定制汽车外饰件?3DeVOK MT+ QUICKSURFACE逆向设计解决方案
python·3d·汽车·机器翻译
a努力。2 小时前
中国邮政Java面试被问:MySQL的ICP(索引条件下推)优化原理
java·开发语言·数据仓库·面试·职场和发展·重构·maven
青槿吖2 小时前
【趣味图解】线程同步与通讯:从抢奶茶看透synchronized、ReentrantLock和wait/notify
java·开发语言·jvm·算法
2401_838472512 小时前
C++20概念(Concepts)入门指南
开发语言·c++·算法
yong99902 小时前
基于MATLAB的GFSK调制解调实现
开发语言·matlab
郝学胜-神的一滴2 小时前
Python中的with语句与try语句:资源管理的两种哲学
linux·服务器·开发语言·python·程序人生·算法