6.Python基础:异常

在 Python 程序开发和运行过程中,“异常” 是不可避免的 —— 它可能源于代码逻辑错误(如除以 0)、外部环境变化(如文件不存在)或用户操作不当(如输入非数字)。若不处理异常,程序会直接崩溃并抛出错误信息;通过合理的异常处理,可让程序优雅地应对错误,保障程序稳定性。

6.1 异常概述

异常(Exception)是程序运行时发生的 “意外错误”,Python 会将异常封装为一个 “异常对象”,包含错误位置、类型和描述信息。若程序未处理异常,Python 解释器会采用默认处理方式:打印异常信息(Traceback)、终止程序运行。

6.1.1 异常示例

以下代码因 “除以 0” 触发ZeroDivisionError异常,程序崩溃并输出异常信息:

print(5 / 0)

运行结果(异常信息):

Traceback (most recent call last):
  File "E:\pyproject\异常\异常示例.py", line 1, in <module>
    print(5 / 0)
ZeroDivisionError: division by zero

异常信息包含三部分核心内容:

  1. Traceback:错误堆栈,显示异常发生的代码路径(从外层到内层);
  2. 错误行号:明确异常发生在哪个文件的哪一行(如line 1);
  3. 异常类型与描述ZeroDivisionError是异常类型,division by zero是异常描述(说明错误原因)。

6.1.2 异常类型

Python 中的所有异常都继承自BaseException类,常用的异常类型及触发场景如下:

异常类型 触发场景 示例
NameError 使用未定义的变量 print(test)(test 未声明)
IndexError 列表 / 元组等序列越界访问 num_list = []; num_list[0]
AttributeError 访问对象不存在的属性或方法 class Car: pass; car = Car(); print(car.color)
FileNotFoundError 打开不存在的文件或目录 open("test.txt")(test.txt 不存在)
ZeroDivisionError 除法运算中除数为 0 5 / 0
ValueError 数据类型正确但值不符合要求 int("abc")(字符串无法转为整数)
TypeError 操作或函数应用于错误类型的对象 1 + "2"(整数与字符串无法相加)

6.1.3 异常的层级关系

Python 异常体系的核心层级如下:

  • BaseException:所有异常的基类;
    • KeyboardInterrupt:用户按下Ctrl+C中断程序;
    • SystemExit:Python 解释器退出;
  • Exception:所有 “非退出类” 异常的基类(开发中主要处理此类异常);
    • ZeroDivisionErrorIndexErrorFileNotFoundError等:具体异常类型。

开发中通常捕获Exception类(或其子类),避免捕获BaseException(防止捕获到SystemExit等退出异常,导致程序无法正常退出)。

6.2 异常捕获语句

Python 提供try-except语句捕获并处理异常,可根据需求组合elsefinally子句,实现更灵活的异常处理逻辑。

6.2.1 基础语法:try-except

try-except是最核心的异常捕获结构,用于 “监控可能出错的代码”,并在异常发生时执行处理逻辑。

语法格式

try:
    # 可能触发异常的代码块(监控区域)
    代码1
    代码2
except [异常类型1 [as 变量名1]]:
    # 捕获到“异常类型1”时执行的处理逻辑
    处理代码1
except [异常类型2 [as 变量名2]]:
    # 捕获到“异常类型2”时执行的处理逻辑
    处理代码2
...

关键说明

  • try块:必须有,用于包裹 “可能出错的代码”,若代码无异常,except块会被跳过;
  • except块:可多个,用于捕获指定类型的异常,[异常类型]省略时捕获所有Exception类异常;
  • as 变量名:可选,将捕获到的异常对象赋值给变量,可通过变量获取异常详情(如print(变量名)打印异常描述)。

示例 1:捕获单个异常

# 监控“除法运算”,捕获ZeroDivisionError
num1 = int(input("请输入被除数:"))
num2 = int(input("请输入除数:"))

try:
    result = num1 / num2
    print(f"结果:{result}")
except ZeroDivisionError as e:
    # 处理“除数为0”的异常
    print(f"出错了:{e}")  # 输出:出错了:division by zero

示例 2:捕获多个异常

# 监控“输入+除法”,捕获ValueError和ZeroDivisionError
try:
    num1 = int(input("请输入被除数:"))  # 可能触发ValueError(输入非数字)
    num2 = int(input("请输入除数:"))    # 可能触发ValueError
    result = num1 / num2                # 可能触发ZeroDivisionError
    print(f"结果:{result}")
except ValueError as e:
    print(f"输入错误:{e}")  # 处理“输入非数字”
except ZeroDivisionError as e:
    print(f"计算错误:{e}")  # 处理“除数为0”

示例 3:捕获所有异常(不推荐)

# 捕获所有Exception类异常(适合临时调试,不推荐正式环境使用)
try:
    num1 = int(input("请输入被除数:"))
    num2 = int(input("请输入除数:"))
    result = num1 / num2
    print(f"结果:{result}")
except Exception as e:
    print(f"程序出错:{e}")  # 所有异常都会进入这里处理

6.2.2 扩展语法 1:try-except-else

else子句用于定义 “try块无异常时执行的逻辑”—— 仅当try块中的代码完全无异常时,else块才会执行;若try块触发异常,else块会被跳过。

语法格式

try:
    可能出错的代码
except [异常类型] as e:
    异常处理代码
else:
    无异常时执行的代码

示例

try:
    num1 = int(input("请输入被除数:"))
    num2 = int(input("请输入除数:"))
    result = num1 / num2
except (ValueError, ZeroDivisionError) as e:
    print(f"出错了:{e}")
else:
    # 仅当try块无异常时执行
    print(f"计算成功!结果:{result}")

6.2.3 扩展语法 2:try-except-finally

finally子句用于定义 “无论try块是否有异常,都必须执行的逻辑”—— 通常用于 “资源清理”(如关闭文件、关闭网络连接),确保资源不会因异常而泄漏。

语法格式

try:
    可能出错的代码
except [异常类型] as e:
    异常处理代码
finally:
    必须执行的代码(无论是否有异常)

示例:关闭文件资源

try:
    # 尝试打开并读取文件
    file = open('f:\\luckycloud.txt', 'r', encoding='utf-8')
    print(file.read())
except FileNotFoundError as e:
    print(f"文件操作出错:{e}")
finally:
    # 无论是否有异常,都关闭文件
    if 'file' in locals():  # 判断file变量是否存在(避免未打开文件时调用close())
        file.close()
        print("文件已关闭")

6.2.4 组合语法:try-except-else-finally

四个子句可组合使用,执行顺序为:

  1. 执行try块;
  2. try块有异常:执行对应except块 → 执行finally块;
  3. try块无异常:执行else块 → 执行finally块。

示例

try:
    num1 = int(input("请输入被除数:"))
    num2 = int(input("请输入除数:"))
    result = num1 / num2
except (ValueError, ZeroDivisionError) as e:
    print(f"出错了:{e}")
else:
    print(f"计算成功!结果:{result}")
finally:
    print("程序执行完毕(无论是否有异常,我都会执行)")

6.3 抛出异常

除了 “程序自动触发异常”,Python 还支持通过raiseassert语句 “主动抛出异常”,用于在自定义场景下(如数据校验失败)触发错误,控制程序流程。

6.3.1 raise语句:显式抛出异常

raise语句用于主动抛出指定类型的异常,可自定义异常描述信息,是开发中最常用的 “主动抛异常” 方式。

语法格式

raise语句有三种常用格式:

  1. raise 异常类:抛出指定类型的异常(无描述信息);
  2. raise 异常类对象:抛出指定类型的异常(可自定义描述信息);
  3. raise:在except块中重新抛出 “刚捕获的异常”(用于向上传递异常)。

示例 1:抛出无描述的异常

# 抛出IndexError异常
raise IndexError

运行结果:

Traceback (most recent call last):
  File "E:\pyproject\异常\raise示例1.py", line 2, in <module>
    raise IndexError
IndexError

示例 2:抛出带描述的异常

# 抛出带描述信息的IndexError
index_error = IndexError("索引下标超出列表范围")
raise index_error

运行结果:

Traceback (most recent call last):
  File "E:\pyproject\异常\raise示例2.py", line 3, in <module>
    raise index_error
IndexError: 索引下标超出列表范围

示例 3:重新抛出异常

except块中捕获异常后,可通过raise重新抛出该异常,让上层代码处理(用于 “异常传递”):

def func():
    try:
        5 / 0  # 触发ZeroDivisionError
    except ZeroDivisionError as e:
        print(f"函数内部捕获异常:{e}")
        raise  # 重新抛出异常,让调用者处理

# 调用func(),处理重新抛出的异常
try:
    func()
except ZeroDivisionError as e:
    print(f"调用者捕获异常:{e}")

运行结果:

函数内部捕获异常:division by zero
调用者捕获异常:division by zero

示例 4:异常链(raise-from

通过raise 新异常 from 原异常,可建立 “异常链”—— 新异常由原异常触发,便于追踪异常根源:

try:
    5 / 0  # 原异常:ZeroDivisionError
except Exception as original_e:
    # 抛出新异常,并关联原异常
    raise IndexError("下标超出范围") from original_e

运行结果(包含异常链信息):

Traceback (most recent call last):
  File "E:\pyproject\异常\raise_from示例.py", line 2, in <module>
    5 / 0
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "E:\pyproject\异常\raise_from示例.py", line 5, in <module>
    raise IndexError("下标超出范围") from original_e
IndexError: 下标超出范围

6.3.2 assert语句:断言抛出异常

assert语句(断言)用于 “验证某个条件是否成立”—— 若条件成立,程序继续执行;若条件不成立,抛出AssertionError异常。assert通常用于 “调试阶段的逻辑校验”(如验证函数参数是否合法),不推荐在正式环境依赖assert处理业务逻辑(因为 Python 可通过-O参数关闭断言)。

语法格式

assert 条件表达式[, 异常描述信息]

示例:验证除数不为 0

# 断言“除数不为0”,条件不成立则抛出AssertionError
num1 = int(input("请输入被除数:"))
num2 = int(input("请输入除数:"))

assert num2 != 0, "除数不能为0"  # 条件num2 != 0不成立时,抛出异常
result = num1 / num2
print(f"结果:{result}")

运行结果(若输入除数为 0):

请输入被除数:10
请输入除数:0
Traceback (most recent call last):
  File "E:\pyproject\异常\assert示例.py", line 4, in <module>
    assert num2 != 0, "除数不能为0"
AssertionError: 除数不能为0

6.3.3 异常的传递

异常会沿着 “函数调用栈” 向上传递 —— 若函数 A 调用函数 B,函数 B 中触发异常且未处理,异常会传递到函数 A;若函数 A 也未处理,异常会继续向上传递,直到被捕获或导致程序崩溃。

示例:异常传递

# 函数B:触发异常且未处理
def func_b():
    print("进入func_b")
    raise ValueError("func_b中发生错误")  # 触发异常

# 函数A:调用func_b,未处理异常
def func_a():
    print("进入func_a")
    func_b()  # 调用func_b,异常会传递到这里
    print("离开func_a")  # 异常未处理,此句不会执行

# 主程序:调用func_a,处理异常
try:
    func_a()
except ValueError as e:
    print(f"主程序捕获异常:{e}")

运行结果(异常从 func_b 传递到主程序):

进入func_a
进入func_b
主程序捕获异常:func_b中发生错误

6.4 自定义异常

Python 提供的内置异常(如ZeroDivisionErrorIndexError)可满足大部分场景,但在特定业务中(如 “密码长度不足”“账号已锁定”),需自定义异常类,让异常更贴合业务逻辑。

6.4.1 自定义异常的规则

自定义异常需满足以下两点:

  1. 自定义异常类必须继承Exception(或其子类),不能继承BaseException(避免与系统退出异常混淆);
  2. 类名通常以 “Error” 结尾(如ShortPwdErrorAccountLockedError),符合 Python 命名规范,便于识别。

6.4.2 自定义异常的实现

步骤

  1. 定义异常类,继承Exception
  2. (可选)在__init__方法中添加自定义属性(如错误码、详细信息);
  3. 使用raise语句抛出自定义异常。

示例:自定义 “密码长度不足” 异常

# 1. 自定义异常类:继承Exception
class ShortPwdError(Exception):
    """自定义异常:密码长度不足"""
    def __init__(self, current_length, min_length):
        # 自定义属性:当前密码长度、最小要求长度
        self.current_length = current_length
        self.min_length = min_length

    # 自定义异常的字符串表示(可选,便于打印异常信息)
    def __str__(self):
        return f"ShortPwdError:输入的密码长度为{self.current_length},至少需要{self.min_length}个字符"

# 2. 使用自定义异常
try:
    pwd = input("请输入密码:")
    if len(pwd) < 3:
        # 抛出自定义异常
        raise ShortPwdError(len(pwd), 3)
    print("密码设置成功!")
except ShortPwdError as e:
    # 处理自定义异常
    print(e)  # 输出:ShortPwdError:输入的密码长度为2,至少需要3个字符

6.5 实训案例

6.5.1 头像格式检测

需求

某网站仅允许用户上传jpgpngjpeg格式的头像文件,需通过异常处理实现以下功能:

  1. 接收用户输入的文件路径;
  2. 检测文件扩展名是否在允许范围内,若不在,抛出UnsupportedFormatError(自定义异常);
  3. 捕获异常并提示用户 “不支持的文件格式,请上传 jpg/png/jpeg 文件”;
  4. 若文件路径不存在,捕获FileNotFoundError并提示用户 “文件不存在,请检查路径”。

实现思路

  1. 自定义UnsupportedFormatError异常类,继承Exception
  2. 编写检测函数:提取文件扩展名,判断是否在允许列表(['jpg', 'png', 'jpeg'])中,不在则抛出自定义异常;
  3. 使用try-except捕获 “文件不存在” 和 “格式不支持” 两种异常,分别处理。

参考代码

import os

# 1. 自定义异常:不支持的文件格式
class UnsupportedFormatError(Exception):
    def __init__(self, file_format, allowed_formats):
        self.file_format = file_format
        self.allowed_formats = allowed_formats

    def __str__(self):
        return f"UnsupportedFormatError:不支持{self.file_format}格式,仅允许{self.allowed_formats}格式"

# 2. 检测头像格式
def check_avatar_format(file_path):
    # 提取文件扩展名(转为小写)
    _, ext = os.path.splitext(file_path)  # ext格式为“.jpg”
    file_format = ext.lstrip('.').lower()  # 去除“.”,转为小写,如“jpg”

    # 允许的格式列表
    allowed_formats = ['jpg', 'png', 'jpeg']
    if file_format not in allowed_formats:
        # 抛出自定义异常
        raise UnsupportedFormatError(file_format, allowed_formats)
    
    print(f"文件格式检测通过:{file_format}")

# 3. 主逻辑:接收输入并处理异常
if __name__ == "__main__":
    file_path = input("请输入头像文件路径:")
    try:
        # 检查文件是否存在(os.path.exists返回True/False)
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在:{file_path}")
        # 检查文件格式
        check_avatar_format(file_path)
        print("头像上传成功!")
    except FileNotFoundError as e:
        print(f"错误:{e}")
    except UnsupportedFormatError as e:
        print(f"错误:{e}")

6.5.2 海鲜超市数量检测

需求

用户在海鲜超市网购时,需选择海鲜种类和购买数量,需实现以下功能:

  1. 接收用户输入的 “海鲜名称” 和 “购买数量”;
  2. 检测数量是否为正整数(≥1):
    • 若输入非数字,捕获ValueError,提示 “数量需为数字”,并默认数量为 1;
    • 若输入数字但小于 1,捕获ValueError(主动抛出),提示 “数量需≥1”,并默认数量为 1;
  3. 输出最终的 “购买信息”(海鲜名称、数量)。

实现思路

  1. 编写数量检测函数:尝试将输入转为整数,若转换失败抛ValueError;若转换后数量 < 1,也抛ValueError
  2. 使用try-except捕获ValueError,统一处理 “非数字” 和 “数量不足” 两种错误,设置默认数量为 1;
  3. 输出最终购买信息。

参考代码

# 检测购买数量是否合法
def check_quantity(input_quantity):
    try:
        # 尝试将输入转为整数
        quantity = int(input_quantity)
    except ValueError:
        # 输入非数字,抛出异常
        raise ValueError("数量需为数字")
    
    # 数量小于1,抛出异常
    if quantity < 1:
        raise ValueError("数量需≥1")
    
    return quantity

# 主逻辑
if __name__ == "__main__":
    # 接收用户输入
    seafood_name = input("请输入海鲜名称:")
    input_quantity = input("请输入购买数量:")

    try:
        # 检测数量
        quantity = check_quantity(input_quantity)
    except ValueError as e:
        # 处理异常,设置默认数量为1
        print(f"输入错误:{e},已默认设置数量为1")
        quantity = 1

    # 输出购买信息
    print(f"\n购买信息:")
    print(f"海鲜名称:{seafood_name}")
    print(f"购买数量:{quantity}")

6.6 本章小结

本章围绕 Python 异常处理展开,核心内容包括:

  1. 异常的基本概念:异常是程序运行时的意外错误,Python 将其封装为异常对象,包含错误位置、类型和描述;
  2. 异常捕获语句:try-except(捕获指定异常)、try-except-else(无异常时执行)、try-except-finally(无论是否有异常都执行),掌握不同场景下的语法选择;
  3. 主动抛出异常:raise(显式抛出指定异常,支持异常链)、assert(断言条件,调试阶段用),理解异常传递的规则;
  4. 自定义异常:继承Exception类,定义贴合业务的异常类型,让异常处理更精准;
  5. 实训案例:通过 “头像格式检测”“海鲜数量检测”,掌握异常处理在实际业务中的应用。

通过本章学习,需理解 “异常处理的核心目标”—— 不是避免异常,而是让程序在异常发生时 “不崩溃、有反馈、可恢复”,保障程序的稳定性和用户体验。在实际开发中,需根据业务场景合理选择异常类型、设计处理逻辑,避免 “捕获所有异常”“忽略异常” 等不良实践。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐