Python3 基础教程

Python3 异常处理进阶

🎉摘要:本文详细讲解Python异常处理完整机制,包括try-except-else-finally语法、多异常分支捕获、合并同类异常、自定义异常类、raise主动抛出与异常链、assert断言的使用与陷阱,以及异常处理最佳实践(避免裸except、不过度捕获、finally中不抛异常、与日志结合)。通过丰富示例帮助开发者编写健壮代码。

异常是程序运行时的错误信号。掌握异常处理的完整机制,能让代码在错误面前优雅降级、保留调试信息,而不是直接崩溃。

异常完整结构

完整的异常语法块结构语法如下:

try:
    # 可能抛出异常的逻辑
    statements
except 异常类型1 as err1:
    # 匹配异常1时执行
    handle1
except (异常类型2, 异常类型3) as err2:
    # 多种异常共用一套处理逻辑
    handle2
except 异常类型4:
    handle3
else:
    # try 代码块没有发生任何异常才执行
    else_statements
finally:
    # 无论是否异常、是否return、是否break,必定执行
    finally_statements

例如,下面以除法运算为例:

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError as e:
        print(f"[ERROR] 除零错误: {e}")
        return None
    else:
        # 无异常时才会执行
        print(f"[INFO] 计算成功: {result}")
        return result
    finally:
        # 无论是否异常,都会执行(用于资源清理)
        print("[INFO] 清理工作完成")

if __name__ == "__main__":
    divide(10, 2)   # 正常路径
    divide(10, 0)   # 异常路径

运行代码,输出如下:

[INFO] 计算成功: 5.0
[INFO] 清理工作完成
[ERROR] 除零错误: division by zero
[INFO] 清理工作完成

执行顺序:

  1. try 块:执行业务逻辑

  2. except:捕获指定异常时执行

  3. else:无异常时才执行,适合放依赖成功结果的逻辑

  4. finally:无论是否异常都执行,适合关闭文件、释放锁、断开连接

多异常分支捕获

多异常分支语法如下:

try:
    可能抛出异常的代码
except 异常类型1:
    处理1
except 异常类型2:
    处理2
except (异常3, 异常4):
    统一处理多种异常
except Exception as e:
    捕获其余所有常规异常
else:
    try内无异常才执行
finally:
    无论是否异常,一定会执行

从上到下匹配,匹配到第一个符合的分支就不再往下走。例如:

import json
from typing import Any

def parse_config(raw: str) -> Any:
    try:
        obj = json.loads(raw)
        port = obj["server"]["port"]
    except json.JSONDecodeError as e:
        print(f"[ERROR] JSON 解析失败: {e}")
        return None
    except KeyError as e:
        print(f"[ERROR] 缺少必要字段: {e}")
        return None
    except TypeError as e:
        print(f"[ERROR] 类型错误(期望字典,得到列表/字符串): {e}")
        return None
    else:
        print(f"[INFO] 服务器端口: {port}")
        return port

if __name__ == "__main__":
    parse_config('{"server": {"port": 8080}}')      # 正常
    parse_config('不是 json')                          # JSONDecodeError
    parse_config('{"server": 123}')                  # TypeError
    parse_config('{"server": {"host": "0.0.0.0"}}')  # KeyError

运行代码,输出:

[INFO] 服务器端口: 8080
[ERROR] JSON 解析失败: Expecting value: line 1 column 1 (char 0)
[ERROR] 类型错误(期望字典,得到列表/字符串): 'int' object is not subscriptable
[ERROR] 缺少必要字段: 'port'

注意异常捕获顺序原则为:先子类,后父类。如果 except Exception 写在最前面,后面的具体异常永远不会被触发。例如:

# 错误顺序示范(不要这样做)
try:
    1 / 0
except Exception as e:          # 吞掉了所有异常
    print(f"通用错误: {e}")
except ZeroDivisionError as e:  # 这行永远不会执行
    print(f"除零错误: {e}")

合并同类异常

如果多个异常写在同一个 except(元组),则共用一套处理逻辑。例如:

def fetch_data():
    try:
        # 模拟网络或文件操作可能抛出多种异常
        import random
        exc = random.choice([IOError, ValueError, ConnectionError])
        raise exc("模拟错误")
    except (IOError, ConnectionError) as e:
        print(f"[ERROR] IO/网络错误: {e}")
    except ValueError as e:
        print(f"[ERROR] 数据格式错误: {e}")

if __name__ == "__main__":
    for _ in range(5):
        fetch_data()

运行代码,输出:

[ERROR] IO/网络错误: 模拟错误
[ERROR] IO/网络错误: 模拟错误
[ERROR] 数据格式错误: 模拟错误
[ERROR] IO/网络错误: 模拟错误
[ERROR] IO/网络错误: 模拟错误

自定义异常类

Python 内置的异常是有限的,不可能满足所有业务场景。此时,就需要我们自定义异常类。所有自定义异常必须继承自 Exception 或它的子类。

注意,不要直接继承 BaseException(会包含键盘中断、系统退出等非业务异常)。

自定义异常最简模板如下:

# 自定义业务异常
class BusinessError(Exception):
    pass

# 使用自定义异常
raise BusinessError("业务执行失败")

例如,自定义业务通用异常,然后继承业务通用异常自定义参数校验、权限不足和资源不存在。

class BusinessError(Exception):
    """业务通用异常"""
    pass

class ValidationError(BusinessError):
    """参数校验失败"""
    pass

class PermissionError(BusinessError):
    """权限不足"""
    pass

class ResourceNotFoundError(BusinessError):
    """资源不存在"""
    pass

# 使用自定义异常
def get_user(user_id: int):
    if user_id <= 0:
        # 抛出参数校验异常
        raise ValidationError(f"user_id 必须大于 0,当前: {user_id}")
    if user_id == 404:
        # 抛出资源不存在异常
        raise ResourceNotFoundError(f"用户 {user_id} 不存在")
    if user_id == 403:
        # 抛出无权访问异常
        raise PermissionError(f"无权访问用户 {user_id}")
    return {"id": user_id, "name": "Alice"}

if __name__ == "__main__":
    for uid in [0, 404, 403, 1]:
        try:
            print(get_user(uid))
        except BusinessError as e:
            print(f"[业务错误] {type(e).__name__}: {e}")

运行代码,输出:

[业务错误] ValidationError: user_id 必须大于 0,当前: 0
[业务错误] ResourceNotFoundError: 用户 404 不存在
[业务错误] PermissionError: 无权访问用户 403
{'id': 1, 'name': 'Alice'}

带额外信息的异常类

上面自定义异常均不能传递参数,下面演示自定义异常允许传递参数信息,例如:

from typing import Any

class ApiError(Exception):
    """带 HTTP 状态码和业务代码的异常"""

    def __init__(self, message: str, status_code: int = 500, code: str = "INTERNAL_ERROR"):
        super().__init__(message)
        self.status_code = status_code
        self.code = code
        self.message = message

    def __repr__(self) -> str:
        return f"ApiError(status_code={self.status_code}, code={self.code}, message={self.message!r})"

    def to_dict(self) -> dict[str, Any]:
        return {
            "code": self.code,
            "message": self.message,
            "status_code": self.status_code,
        }

class NotFoundError(ApiError):
    def __init__(self, resource: str, resource_id: Any):
        super().__init__(
            message=f"{resource} {resource_id} 不存在",
            status_code=404,
            code="NOT_FOUND"
        )

class UnauthorizedError(ApiError):
    def __init__(self, detail: str = "身份验证失败"):
        super().__init__(
            message=detail,
            status_code=401,
            code="UNAUTHORIZED"
        )

if __name__ == "__main__":
    try:
        raise NotFoundError("用户", 12345)
    except ApiError as e:
        print(f"异常: {e}")
        print(f"字典: {e.to_dict()}")

运行代码,输出:

异常: 用户 12345 不存在
字典: {'code': 'NOT_FOUND', 'message': '用户 12345 不存在', 'status_code': 404}

异常设计建议:

  • 自定义异常应该继承 Exception 或更具体的内置异常(如 ValueError)

  • 不要直接继承 BaseException,否则会捕获 SystemExit 和 KeyboardInterrupt

  • 提供 __init__ 接收上下文信息,方便上层记录日志或返回给客户端

raise 主动抛出与异常链

主动抛出异常

raise 用于手动触发异常,中断正常流程,交由上层 try-except 捕获处理。例如:

from typing import Any

def transfer(from_id: int, to_id: int, amount: float) -> dict[str, Any]:
    if amount <= 0:
        raise ValueError("转账金额必须大于 0")
    if from_id == to_id:
        raise ValueError("不能给自己转账")
    if amount > 1_000_000:
        raise PermissionError("单笔转账超过限额,需要人工审批")

    # 模拟业务逻辑
    return {"from": from_id, "to": to_id, "amount": amount, "status": "success"}

if __name__ == "__main__":
    for case in [(1, 2, 100), (1, 1, 100), (1, 2, -50), (1, 2, 2_000_000)]:
        try:
            result = transfer(*case)
            print(f"[OK] {result}")
        except (ValueError, PermissionError) as e:
            print(f"[FAIL] {type(e).__name__}: {e}")

运行代码,输出如下:

[OK] {'from': 1, 'to': 2, 'amount': 100, 'status': 'success'}
[FAIL] ValueError: 不能给自己转账
[FAIL] ValueError: 转账金额必须大于 0
[FAIL] PermissionError: 单笔转账超过限额,需要人工审批

raise ... from e 异常链

在封装底层异常时,保留原始堆栈信息对调试至关重要。例如,底层抛出了异常 A,被封装成异常 B,异常 B 被封装成异常 C,最终将异常 C 抛出。如果没有异常链,用户看见异常 C,根本不知道真实的异常信息和异常抛出地点。无法准确找到错误进行修复。

例如:配置未找到和配置格式错误,均通过 from e 将原始异常添加到异常链

import json
from typing import Any

class ConfigError(Exception):
    """配置加载失败"""
    pass

def load_config(path: str) -> dict[str, Any]:
    try:
        with open(path, "r", encoding="utf-8") as f:
            raw = f.read()
    except FileNotFoundError as e:
        # 用 raise ... from e 保留原始 FileNotFoundError 的堆栈
        raise ConfigError(f"配置文件未找到: {path}") from e

    try:
        return json.loads(raw)
    except json.JSONDecodeError as e:
        raise ConfigError(f"配置文件 {path} JSON 格式错误,行 {e.lineno}, 列 {e.colno}") from e

if __name__ == "__main__":
    # 情况 1:文件不存在
    try:
        load_config("/不存在的路径/config.json")
    except ConfigError as e:
        print(f"\n[错误] {e}")
        print(f"[原始异常] {e.__cause__}")

    # 情况 2:JSON 格式错误
    import tempfile, os
    with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as f:
        f.write("{ 不是合法 json")
        tmp_path = f.name
    try:
        load_config(tmp_path)
    except ConfigError as e:
        print(f"\n[错误] {e}")
        print(f"[原始异常] {type(e.__cause__).__name__}: {e.__cause__}")
    finally:
        os.unlink(tmp_path)

运行代码,输出如下:

[错误] 配置文件未找到: /不存在的路径/config.json
[原始异常] [Errno 2] No such file or directory: '/不存在的路径/config.json'

[错误] 配置文件 C:\Users\ADMINI~1\AppData\Local\Temp\tmpzfk8362s.json JSON 格式错误,行 1, 列 3
[原始异常] JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 3 (char 2)

异常链的好处:

  • 上层调用方只关心 ConfigError 语义

  • 调试时通过 __cause__ 可追溯到原始失败位置(文件不存在?JSON 格式错误?)

  • 打印 traceback 时会自动显示 The above exception was the direct cause of the following exception:

使用 raise ... from None 抑制上下文

在某些场景下,不希望暴露内部细节(如安全敏感信息),可以使用 from None 隐藏内部异常细节。

import hashlib

class AuthError(Exception):
    """认证失败(不暴露内部原因)"""
    pass

def verify_password(stored_hash: str, password: str) -> None:
    try:
        # 假设某种复杂的密码验证逻辑
        if stored_hash != hashlib.sha256(password.encode()).hexdigest():
            raise ValueError("密码不匹配")
    except Exception:
        # 隐藏内部异常细节,防止攻击者推断系统逻辑
        raise AuthError("用户名或密码错误") from None

if __name__ == "__main__":
    try:
        verify_password("abc123", "wrong")
    except AuthError as e:
        print(f"[认证失败] {e}")
        # e.__cause__ 为 None,原始异常被隐藏
        print(f"[原始异常] {e.__cause__}")

运行代码,输出:

[认证失败] 用户名或密码错误
[原始异常] None

注意:from None 只在安全敏感场景下使用。一般开发阶段建议保留完整链式信息。

assert 断言的使用与陷阱

assert 是开发调试工具,可被优化关闭,绝对不能用来做面向用户的业务校验。外部参数校验一律使用 if + raise 进行验证。

assert 语法如下:

assert 条件 [, 错误描述信息]

等价于:

if not 条件:
    raise AssertionError(错误描述)

注意,断言可以全局关闭。在 Python 启动时增加 -O(大写字母 O,optimize)参数,所有 assert 语句会被直接删除,不再执行!

python -O main.py

一旦开启优化模式:

assert age >= 0, "年龄不能负数"
# 这一行代码直接消失,判断彻底失效!

例如:使用 assert 验证 width 和 height 参数是否大于 0。

class Rectangle:
    def __init__(self, width: float, height: float):
        assert width > 0, "width 必须为正数"
        assert height > 0, "height 必须为正数"
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

if __name__ == "__main__":
    r = Rectangle(3, 4)
    print(f"面积: {r.area()}")

    # 触发断言失败(运行 python -O 时会被优化掉,不会执行)
    try:
        Rectangle(-1, 4)
    except AssertionError as e:
        print(f"[断言失败] {e}")

运行代码,输出:

面积: 12
[断言失败] width 必须为正数

断言 vs 异常的选择:

场景推荐方式原因
内部程序员的假设(如私有方法)assert发现 bug 时立即崩溃,开发阶段暴露问题
用户输入或外部数据校验raise ValueError断言在 python -O 下会被优化掉,不能用于生产校验
必须始终执行的校验if + raise即使优化模式也生效

异常处理最佳实践

不要裸 except

裸 except 会捕获 SystemExit、KeyboardInterrupt,导致程序无法 Ctrl+C 退出。例如:

try:
    do_something()
except: # 不要这样做!
    pass

# 正确:至少捕获 Exception
try:
    do_something()
except Exception as e:
    logger.error("操作失败: %s", e)

不要过度捕获

捕获太宽泛,吞掉了本不该处理的异常。例如:

def parse_int(s: str) -> int:
    try:
        return int(s)
    except Exception:     # 吞掉了 TypeError、MemoryError 等
        return 0

# 正确:只捕获预期的异常
def parse_int(s: str) -> int | None:
    try:
        return int(s)
    except ValueError:
        return None

在 finally 中不要抛出新异常

finally 中的异常会覆盖 try 中的异常,导致异常被覆盖,掩藏真实的异常信息。例如:

try:
    1 / 0
finally:
    raise RuntimeError("清理失败")   # 原始的 ZeroDivisionError 被丢失!

# 正确:用 try/except 保护 finally 中的清理逻辑
try:
    1 / 0
finally:
    try:
        cleanup()
    except Exception as e:
        logger.warning("清理失败: %s", e)

异常处理与日志结合

将异常处理机制与日志记录能力结合,在程序运行过程中对各类异常情况进行完整捕获、合理处理的同时,同步完成关键事件、报错堆栈、上下文运行参数等信息的日志留存,便于后续问题定位、故障复盘以及运行状态分析。例如:

import logging
import traceback

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)

class DatabaseError(Exception):
    pass

def query_db(sql: str) -> list[dict]:
    try:
        # 模拟数据库连接
        if "DROP" in sql.upper():
            raise SyntaxError("检测到危险 SQL")
        return [{"id": 1, "name": "Alice"}]
    except SyntaxError as e:
        logger.error("SQL 语法错误: %s", e)
        raise DatabaseError("查询执行失败") from e
    except Exception as e:
        # 记录完整堆栈
        logger.error("未预期异常:\n%s", traceback.format_exc())
        raise

if __name__ == "__main__":
    try:
        query_db("SELECT * FROM users")
        query_db("DROP TABLE users")
    except DatabaseError as e:
        logger.critical("业务层捕获: %s", e)

运行代码,输出:

2026-08-13 15:43:40,064 [ERROR] SQL 语法错误: 检测到危险 SQL
2026-08-13 15:43:40,064 [CRITICAL] 业务层捕获: 查询执行失败


说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号