Python 学习笔记(第九期)——异常处理:捕获、抛出与自定义异常

更多Python知识:Python基础入门

我的主页:AOwhisky,这里有更多运维系统性知识整理和其他有趣内容,欢迎与我一起探讨学习~

📖 引言:程序世界的“安全气囊”

在前八期中,我们编写的程序都假设一切“正常”——用户会输入正确的数据、文件一定存在、网络永远畅通。但现实世界远没有那么理想:用户可能输入字母而不是数字,要打开的文件可能被误删,网络请求可能超时……

当这些“意外”发生时,程序会怎样?答案是——程序会崩溃,并抛出一堆普通人看不懂的错误信息。就像一辆没有安全气囊的汽车,一撞就散架。

异常处理就是程序世界的“安全气囊”——它让程序在遇到错误时不会彻底崩溃,而是能够“优雅地”处理问题,要么给出友好的错误提示,要么尝试备选方案。本期我们将系统学习 Python 中的异常处理机制,让程序变得更加健壮和可靠。

— Compiled and Authored by Whisky — July 17 th, 2026

📑 本期目录

  1. 异常概述(什么是异常、常见异常类型)
  2. 异常捕获语句(try-except 基础)
  3. 捕获多种异常(多 except 与元组捕获)
  4. else 子句与 finally 子句
  5. 抛出异常(raise 语句)
  6. 断言(assert 语句)
  7. 异常的传递机制
  8. 自定义异常
  9. 综合实战演练
  10. 总结与知识点一览表

正文

一、异常概述

1.1 什么是异常

异常(Exception)是程序在运行过程中发生的错误事件。当 Python 遇到无法继续执行的错误时,会创建一个异常对象并“抛出”(raise)。如果程序中没有处理这个异常的代码,程序会终止执行并显示错误信息(称为“回溯 Traceback”)。

示例:一个典型的异常信息

print(5 / 0)

运行结果

Traceback (most recent call last):
  File "demo.py", line 1, in <module>
    print(5 / 0)
          ~~^~~
ZeroDivisionError: division by zero

异常信息的组成部分

部分 说明 示例
Traceback 头部 异常发生的位置(文件名、行号) File "demo.py", line 1
错误代码行 引发异常的代码 print(5 / 0)
异常类型 异常的具体分类 ZeroDivisionError
异常描述 具体的错误信息 division by zero
1.2 常见异常类型

Python 内置了丰富的异常类型,大多数继承自 Exception 类(而 Exception 又继承自 BaseException)。

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   └── OverflowError
    ├── AttributeError
    ├── FileNotFoundError
    ├── IndexError
    ├── KeyError
    ├── NameError
    ├── TypeError
    └── ValueError

常见异常速查表

异常类型 触发场景 示例
ZeroDivisionError 除数为 0 10 / 0
NameError 使用未定义的变量 print(undefined_var)
TypeError 类型不匹配的操作 "10" + 5
ValueError 值不符合预期 int("abc")
IndexError 索引超出范围 [1,2,3][5]
KeyError 字典键不存在 {"a":1}["b"]
AttributeError 对象没有该属性 "hello".length
FileNotFoundError 文件不存在 open("notexist.txt")

💡 白话理解:异常就像看病时的“症状”——不同的症状对应不同的疾病(异常类型),对症下药才能治好病(正确地处理异常)。

二、异常捕获语句——try-except

2.1 基础语法
try:
    # 可能出错的代码
    pass
except [异常类型 [as 变量名]]:
    # 捕获异常后的处理代码
    pass

最简单的形式:捕获所有异常(不推荐)。

try:
    num = int(input("请输入数字:"))
    result = 10 / num
    print(result)
except:          # 捕获所有异常
    print("发生了错误!")

⚠️ 不推荐:捕获所有异常会掩盖具体错误类型,不利于定位问题。应该尽量捕获具体的异常类型。

2.2 捕获具体异常
try:
    num = int(input("请输入数字:"))
    result = 10 / num
    print(result)
except ZeroDivisionError:
    print("❌ 除数不能为 0!")
except ValueError:
    print("❌ 请输入有效的数字!")

运行示例

请输入数字:0
❌ 除数不能为 0!

请输入数字:abc
❌ 请输入有效的数字!

请输入数字:5
2.0
2.3 捕获异常并获取异常对象

使用 as 变量名 可以捕获异常对象,获取详细的错误信息。

try:
    num = int(input("请输入数字:"))
    result = 10 / num
    print(result)
except ZeroDivisionError as e:
    print(f"❌ 错误类型:{type(e).__name__},信息:{e}")
except ValueError as e:
    print(f"❌ 错误类型:{type(e).__name__},信息:{e}")

运行示例

请输入数字:abc
❌ 错误类型:ValueError,信息:invalid literal for int() with base 10: 'abc'
2.4 多个异常合并捕获

使用元组将多个异常合并到一个 except 中捕获。

try:
    num = int(input("请输入数字:"))
    result = 10 / num
    print(result)
except (ZeroDivisionError, ValueError) as e:
    print(f"❌ 输入错误:{e}")
2.5 捕获父类异常

Exception 是所有内置异常的父类,可以捕获所有继承自 Exception 的异常。

try:
    num = int(input("请输入数字:"))
    result = 10 / num
    print(result)
except Exception as e:
    print(f"❌ 发生异常:{type(e).__name__} - {e}")

⚠️ 使用 Exception 的注意事项

  • 优点:能捕获所有异常,程序不会崩溃
  • 缺点:掩盖了具体异常类型,不利于调试
  • 最佳实践:在顶层使用 Exception 做“兜底捕获”,下层使用具体异常类型做精细处理

三、else 子句与 finally 子句

3.1 else 子句——没有异常时执行

else 子句中的代码仅在 try没有抛出任何异常时执行。

try:
    num = int(input("请输入数字:"))
    result = 10 / num
except ZeroDivisionError:
    print("❌ 除数不能为 0!")
except ValueError:
    print("❌ 请输入有效的数字!")
else:
    # 只有没有异常时才会执行
    print(f"✅ 计算成功!结果是:{result}")

运行示例

请输入数字:5
✅ 计算成功!结果是:2.0

请输入数字:0
❌ 除数不能为 0!
3.2 finally 子句——无论如何都执行

finally 子句中的代码无论是否发生异常都会执行,通常用于释放资源(如关闭文件、关闭网络连接)。

try:
    file = open("data.txt", "r", encoding="utf-8")
    content = file.read()
    print(content)
except FileNotFoundError:
    print("❌ 文件不存在!")
finally:
    # 无论是否异常,都要关闭文件
    # 注意:如果文件不存在,file 变量未定义,需要先判断
    if 'file' in locals():
        file.close()
        print("✅ 文件已关闭")

try-except-else-finally 完整结构

try:
    # 1. 可能出错的代码
    pass
except 异常类型1:
    # 2. 捕获特定异常
    pass
except 异常类型2:
    # 3. 捕获另一种异常
    pass
else:
    # 4. 没有异常时执行
    pass
finally:
    # 5. 无论如何都执行(资源清理)
    pass

执行流程总结

情况 try except else finally
无异常 ✅ 执行 ❌ 跳过 ✅ 执行 ✅ 执行
有异常且被捕获 ✅ 执行到异常处 ✅ 执行 ❌ 跳过 ✅ 执行
有异常且未被捕获 ✅ 执行到异常处 ❌ 跳过 ❌ 跳过 ✅ 执行(程序随后崩溃)

四、抛出异常——raise 语句

除了 Python 自动抛出异常,我们也可以使用 raise 语句主动抛出异常。

4.1 基本用法
# 格式1:使用异常类
raise IndexError

# 格式2:使用异常对象(带描述信息)
raise IndexError("索引超出范围")

# 格式3:重新抛出当前异常(在 except 块中使用)
try:
    raise ValueError("值错误")
except ValueError:
    print("捕获到异常,重新抛出")
    raise   # 重新抛出,不改变异常类型和消息

示例:输入校验中的主动抛异常

def set_age(age):
    if age < 0 or age > 120:
        raise ValueError("年龄必须在 0~120 之间")
    print(f"年龄设置为:{age}")

try:
    set_age(150)
except ValueError as e:
    print(f"❌ 设置失败:{e}")

运行结果

❌ 设置失败:年龄必须在 0~120 之间
4.2 raise...from——异常链

当在捕获一个异常后抛出另一个异常时,可以使用 from 保留原始异常信息。

try:
    number = 5 / 0
except ZeroDivisionError as original:
    raise ValueError("计算失败") from original

运行结果

ZeroDivisionError: division by zero

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

Traceback (most recent call last):
  File "demo.py", line 4, in <module>
    raise ValueError("计算失败") from original
ValueError: 计算失败

五、断言——assert 语句

assert(断言)用于检查某个条件是否为 True,如果条件为 False,则抛出 AssertionError 异常。

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

示例

def divide(a, b):
    assert b != 0, "除数不能为 0!"
    return a / b

print(divide(10, 2))   # 5.0
print(divide(10, 0))   # AssertionError: 除数不能为 0!

assert vs raise 对比

对比维度 assert raise
使用场景 调试阶段检查程序内部状态 生产环境主动抛出业务异常
是否可禁用 ✅ 可通过 -O 命令行参数禁用 ❌ 无法禁用
异常类型 固定为 AssertionError 可指定任意异常类型
适用建议 用于检查“不应该发生”的情况 用于检查“可能发生”的合法异常

💡 生产环境建议:断言主要用于开发和测试阶段,在生产环境中应使用 raise 抛出具体的异常类型,而不是依赖断言。

六、异常的传递机制

当函数中发生异常且没有被捕获时,异常会向上传递到调用栈——从当前函数传递到调用它的函数,再到更外层的函数,直到被捕获或程序终止。

func_c() 中发生异常
    ↓(未被捕获)
func_b() 调用 func_c()
    ↓(未被捕获)
func_a() 调用 func_b()
    ↓(未被捕获)
main() 调用 func_a()
    ↓(未被捕获)
程序崩溃并输出 Traceback

示例:异常的传递

def func_c():
    print("func_c 开始")
    raise ValueError("func_c 中的异常")
    print("func_c 结束")   # 不会执行

def func_b():
    print("func_b 开始")
    func_c()
    print("func_b 结束")   # 不会执行

def func_a():
    print("func_a 开始")
    try:
        func_b()
    except ValueError as e:
        print(f"✅ 在 func_a 中捕获异常:{e}")
    print("func_a 结束")

func_a()

运行结果

func_a 开始
func_b 开始
func_c 开始
✅ 在 func_a 中捕获异常:func_c 中的异常
func_a 结束

传递路径

func_c 抛出 ValueError
    ↓ 未被捕获,向上传递
func_b 未捕获
    ↓ 未被捕获,向上传递
func_a 中的 try 捕获了 ValueError
    ✅ 异常被成功处理

💡 白话理解:异常传递就像“逐级上报”——下属解决不了的问题,上报给主管;主管解决不了,上报给经理;经理解决了,问题结束。如果一路上都没人能处理,那整个公司就“宕机”了。

七、自定义异常

当 Python 内置的异常类型不足以描述我们的业务错误时,可以创建自定义异常类

创建方法:继承 Exception 类或其子类,类名通常以 Error 结尾。

class ShortPasswordError(Exception):
    """密码长度不足异常"""
    def __init__(self, length, min_length):
        self.length = length
        self.min_length = min_length
        super().__init__(f"密码长度为 {length},至少需要 {min_length} 位")

def set_password(password):
    min_len = 6
    if len(password) < min_len:
        raise ShortPasswordError(len(password), min_len)
    print(f"✅ 密码设置成功:{password}")

try:
    set_password("123")
except ShortPasswordError as e:
    print(f"❌ {e}")

运行结果

❌ 密码长度为 3,至少需要 6 位

自定义异常的最佳实践

实践 说明
命名规范 类名以 Error 结尾,如 ValidationError
继承体系 创建一个基类异常(如 AppError),所有自定义异常继承它
信息丰富 __init__ 中记录关键信息,便于调试
文档字符串 添加文档字符串说明异常的触发条件

八、综合实战演练

实战一:头像格式检测

需求描述:网站只允许用户上传 jpgpngjpeg 格式的文件。编写程序检测用户上传的文件扩展名,如果不符合要求则抛出异常。

涉及知识点:自定义异常、raiseendswith()、异常捕获

import os

# 自定义异常:文件格式不合法
class InvalidFileFormatError(Exception):
    def __init__(self, filename, allowed_formats):
        self.filename = filename
        self.allowed_formats = allowed_formats
        super().__init__(f"不支持的文件格式:{filename},允许的格式:{', '.join(allowed_formats)}")

def validate_image(filename):
    """验证图片格式是否合法"""
    ALLOWED = ["jpg", "png", "jpeg"]

    # 检查是否有扩展名
    if "." not in filename:
        raise InvalidFileFormatError(filename, ALLOWED)

    # 获取扩展名(转小写)
    ext = filename.rsplit(".", 1)[-1].lower()

    if ext not in ALLOWED:
        raise InvalidFileFormatError(filename, ALLOWED)

    print(f"✅ 文件 {filename} 格式验证通过!")

def upload_avatar(filename):
    """上传头像"""
    try:
        validate_image(filename)
        print(f"📤 正在上传 {filename}...")
        # 模拟上传操作
        print("✅ 上传成功!")
    except InvalidFileFormatError as e:
        print(f"❌ 上传失败:{e}")
    except Exception as e:
        print(f"❌ 未知错误:{e}")

# 测试
upload_avatar("avatar.jpg")     # ✅ 通过
upload_avatar("photo.png")      # ✅ 通过
upload_avatar("image.gif")      # ❌ 不支持
upload_avatar("no_ext")         # ❌ 无扩展名

运行结果

✅ 文件 avatar.jpg 格式验证通过!
📤 正在上传 avatar.jpg...
✅ 上传成功!

✅ 文件 photo.png 格式验证通过!
📤 正在上传 photo.png...
✅ 上传成功!

❌ 上传失败:不支持的文件格式:image.gif,允许的格式:jpg, png, jpeg

❌ 上传失败:不支持的文件格式:no_ext,允许的格式:jpg, png, jpeg
实战二:海鲜超市数量检测

需求描述:用户购买海鲜时,购买数量必须不小于 1。如果用户输入的数量小于 1,程序抛出异常并提示错误信息,然后将数量设为默认值 1。

涉及知识点try-except-elseraise、输入校验、自定义异常

class InvalidQuantityError(Exception):
    """无效数量异常"""
    def __init__(self, quantity, default=1):
        self.quantity = quantity
        self.default = default
        super().__init__(f"数量 {quantity} 无效,已自动设为默认值 {default}")

def get_quantity(prompt):
    """获取用户输入的数量,并进行校验"""
    try:
        quantity = int(input(prompt))
        if quantity < 1:
            raise InvalidQuantityError(quantity)
        return quantity
    except ValueError:
        print("❌ 请输入有效的整数!")
        return get_quantity(prompt)
    except InvalidQuantityError as e:
        print(f"⚠️ 警告:{e}")
        return e.default

def seafood_shop():
    """海鲜超市主程序"""
    print("=" * 40)
    print("      🦐 海鲜超市")
    print("=" * 40)

    # 海鲜价格表
    products = {
        "1": {"name": "龙虾", "price": 188},
        "2": {"name": "帝王蟹", "price": 399},
        "3": {"name": "鲍鱼", "price": 68},
        "4": {"name": "三文鱼", "price": 128},
    }

    # 显示商品列表
    print("商品列表:")
    for key, product in products.items():
        print(f"  {key}. {product['name']}:¥{product['price']}/斤")

    # 选择商品
    choice = input("请选择商品编号(1-4):")
    if choice not in products:
        print("❌ 无效的商品编号!")
        return

    product = products[choice]

    # 获取数量(带校验)
    quantity = get_quantity(f"请输入购买数量(斤):")

    # 计算总价
    total = product["price"] * quantity
    print("\n" + "=" * 40)
    print("        📋 购物小票")
    print("=" * 40)
    print(f"商品:{product['name']}")
    print(f"单价:¥{product['price']}/斤")
    print(f"数量:{quantity} 斤")
    print(f"总价:¥{total:.2f}")
    print("=" * 40)

# 运行
seafood_shop()

运行示例

========================================
      🦐 海鲜超市
========================================
商品列表:
  1. 龙虾:¥188/斤
  2. 帝王蟹:¥399/斤
  3. 鲍鱼:¥68/斤
  4. 三文鱼:¥128/斤
请选择商品编号(1-4):1
请输入购买数量(斤):-5
⚠️ 警告:数量 -5 无效,已自动设为默认值 1

========================================
        📋 购物小票
========================================
商品:龙虾
单价:¥188/斤
数量:1 斤
总价:¥188.00
========================================

九、补充知识:异常处理的最佳实践

实践建议 说明 示例
捕获具体异常 不要使用裸 except:,尽量捕获具体的异常类型 except ValueError as e:
异常粒度要适中 一个 try 块不要包含太多代码,尽量精准定位异常源 每个关键操作单独 try
使用 finally 释放资源 文件、网络、数据库连接等资源在 finally 中释放 finally: file.close()
不要忽略异常 捕获异常后必须有处理逻辑(记录日志、提示用户、重试等) 不能只写 pass
自定义异常要继承 Exception 不要直接继承 BaseException class MyError(Exception):
异常信息要清晰 异常消息应包含足够的上下文信息 raise ValueError(f"年龄 {age} 超出范围")
顶层兜底 在程序入口处捕获 Exception,防止程序意外崩溃 主循环中的 except Exception as e:

📝 总结与知识点一览表

核心知识点汇总

类别 关键字/语法 核心作用
异常捕获 try-except 捕获并处理异常,防止程序崩溃
具体捕获 except ValueError: 捕获特定类型的异常
多异常捕获 except (ValueError, TypeError): 用元组同时捕获多种异常
异常对象 except ValueError as e: 获取异常对象的详细信息
无异常执行 else: try 没有异常时执行
资源释放 finally: 无论是否异常都执行
抛出异常 raise ValueError("msg") 主动抛出指定异常
异常重抛 raise except 中重新抛出当前异常
断言 assert condition, "msg" 条件为 False 时抛出 AssertionError
自定义异常 class MyError(Exception): 定义业务特定的异常类型
异常传递 函数调用栈向上传递 异常未被捕获时向调用者传递

异常处理结构速查

需求 代码模板
基础捕获 try: ... except 异常类型: ...
多异常分别处理 try: ... except A: ... except B: ...
多异常合并处理 try: ... except (A, B) as e: ...
完整结构 try: ... except: ... else: ... finally: ...
主动抛异常 raise ValueError("错误信息")
断言检查 assert 条件, "错误信息"
自定义异常 class MyError(Exception): pass

常见异常速查表

异常类型 触发场景 示例
ZeroDivisionError 除数为 0 10 / 0
NameError 使用未定义变量 print(a)(a 未定义)
TypeError 类型不匹配 "10" + 5
ValueError 值不合法 int("abc")
IndexError 索引越界 [1,2][5]
KeyError 键不存在 {"a":1}["b"]
AttributeError 属性不存在 "hello".length
FileNotFoundError 文件不存在 open("no.txt")
ImportError 导入失败 import 不存在的模块
KeyboardInterrupt 用户按 Ctrl+C 中断程序执行

常见错误排查指南

错误现象 大概率原因 解决方案
程序直接崩溃并输出 Traceback 没有 try-except 捕获异常 在关键代码处添加异常捕获
except: 捕获了所有异常但无法定位问题 使用了裸 except 改为捕获具体异常或 Exception
finally 中访问了未定义的变量 try 中变量创建失败(如文件打开失败) finally 中使用 locals()globals() 检查变量是否存在
assert 在生产环境失效 使用了 -O 优化参数运行 生产环境使用 raise 替代 assert
自定义异常未被捕获 异常类继承错误或捕获了错误的父类 确保自定义异常继承 Exception,且捕获时使用正确的类型
raise 重新抛出丢失了原始异常信息 使用了 raise 新异常 而非 raise 使用 raise 不传参保留原异常,或使用 raise 新异常 from 原异常

学习/生产最佳实践

  1. 不要使用裸 except::它会捕获所有异常(包括 KeyboardInterruptSystemExit),导致程序无法被正常中断。应使用 except Exception as e: 或捕获具体异常。
  2. 异常粒度要适中:不要在同一个 try 块中包裹太多代码,否则难以定位具体是哪里出了错。每个关键操作单独使用 try-except
  3. 资源释放用 finally:无论是文件、数据库连接还是网络套接字,释放资源的代码应该放在 finally 中,确保万无一失。
  4. 异常信息要足够用于调试:抛出异常时提供清晰的上下文信息,如 raise ValueError(f"年龄 {age} 超出有效范围 [0, 120]")
  5. 顶层兜底:在程序的入口处(如 main())设置一个顶层的 try-except,捕获所有未被处理的异常,记录日志并给出友好的用户提示,而不是直接暴露 Traceback。
  6. 自定义异常要有层次:为项目定义一个基类异常(如 AppException),所有业务异常继承它,方便统一捕获和处理。

🔜 下期预告

本期我们系统学习了异常处理——从异常的概述与常见类型,到 try-except 捕获、elsefinally 子句,再到 raise 主动抛出、assert 断言、异常的传递机制以及自定义异常。通过“头像格式检测”和“海鲜超市数量检测”两个实战项目,让程序在面对错误时更加健壮和可靠。

至此,Python 基础系列(第一期至第九期)已全部完结!我们完成了从环境搭建、基础语法、流程控制、字符串、组合数据类型、函数、文件与数据格式化、面向对象编程到异常处理的完整学习路径。感谢一路的坚持与陪伴!

后续,我们将正式开启自动化运维的技能学习,敬请期待下一期~


— Compiled and Authored by Whisky — July 17 th, 2026
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐