page contents

Python 异常处理:写出经得起异常考验的代码

异常处理是 Python 代码质量的底线。写得好,线上问题秒级定位;写得差,一个未被捕获的异常就能让整个服务崩溃。

attachments-2026-09-rwn2FKFm6aac9945a1726.png大部分 Python 程序员对异常处理的认知停留在 try-except,见过最多的代码大概是:

try:
    do_something()
except:
    pass

这种写法在生产环境中是定时炸弹。裸 except 会吞掉 KeyboardInterrupt 和 SystemExit,让服务无法正常停止;空 pass 让错误悄无声息地消失,排查问题时日志里一片空白。

异常处理是 Python 代码质量的底线。写得好,线上问题秒级定位;写得差,一个未被捕获的异常就能让整个服务崩溃。

一、异常层级结构

Python 内置异常有一套清晰的继承体系:

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   └── OverflowError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError
    │   ├── FileNotFoundError
    │   └── PermissionError
    ├── ValueError
    ├── TypeError
    ├── RuntimeError
    └── ImportError

 

关键的区分:except Exception 是安全的(只捕获程序逻辑异常),except BaseException 或裸 except 会覆盖系统级异常,包括 KeyboardInterrupt(Ctrl+C)和 SystemExit,应该避免。

正确的写法:

try:
    risky_operation()
except ValueError as e:
    logger.warning(f"参数错误: {e}")
except Exception as e:
    logger.error(f"未预期的异常: {e}", exc_info=True)

注意两点:具体异常在前、通用异常在后(Python 按顺序匹配 except 子句);exc_info=True 会记录完整的堆栈信息,排查问题时它比异常消息更有价值。

二、finally 与 else 的正确用法

finally 的语义是无论是否发生异常都会执行,典型场景是资源清理:

f = None
try:
    f = open("data.txt")
    content = f.read()
except FileNotFoundError:
    logger.warning("文件不存在,使用默认值")
    content = ""
finally:
    if f:
        f.close()

更好的做法是用 with 语句(上下文管理器),但 finally 在非文件资源场景(如网络连接的断开确认、临时目录的清理)中仍然不可替代。

else 子句是很多人不知道的语法:try 块正常执行完毕、没有异常时触发。

try:
    result = db.query(sql)
except DatabaseError as e:
    logger.error(f"查询失败: {e}")
else:
    cache.set(key, result)
    metrics.record_query_latency(result.elapsed)

把"正常路径的后续逻辑"放在 else 中而不是 try 块里,有两个好处:

异常捕获范围更精确——cache.set() 如果抛异常,不会被上方的 except DatabaseError 错误捕获。

代码意图更清晰——一眼能看出哪些代码可能抛异常,哪些只在成功时执行。

三、自定义异常

当内置异常语义不够精确时,定义自己的异常层级:

class ServiceError(Exception):
    """业务层异常的基类。"""
    ...

class AuthenticationError(ServiceError):
    """认证失败。"""
    ...

class RateLimitError(ServiceError):
    """请求频率超限。"""
    ...

class DataValidationError(ServiceError):
    """数据校验不通过。"""
    def __init__(self, message, field=None, value=None):
        super().__init__(message)
        self.field = field
        self.value = value

自定义异常的价值在于:

精确捕获:调用方可以 except AuthenticationError 只处理认证问题,不让数据校验错误混进来。

携带上下文:DataValidationError 带上了 field 和 value,日志和告警可以直接引用,不需要再解析错误消息字符串。

分层设计:ServiceError 作为基类,在 API 网关层统一 except ServiceError 转换为对应的 HTTP 状态码。

四、raise ... from:异常链

当捕获一个异常后要抛出另一个异常时,用 raise ... from 保留因果链:

def load_config(path):
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise ConfigError(f"配置文件不存在: {path}") from e
    except json.JSONDecodeError as e:
        raise ConfigError(f"JSON 格式错误: {path}") from e

from e 的效果:ConfigError 的 __cause__ 属性会指向原始异常,堆栈信息中会显示两段——底层的 FileNotFoundError 和上层的 ConfigError,中间用 The above exception was the direct cause of the following exception: 连接。

如果想刻意隐藏原始异常(比如出于安全考虑不暴露内部细节),用 raise NewError(...) from None。

五、异常处理与日志

异常处理的第一原则:永远不要悄无声息地吞掉异常。

try:
    process(data)
except Exception:
    pass

try:
    process(data)
except Exception as e:
    print(e)      # 生产环境 stdout 可能没人看

try:
    process(data)
except Exception:
    logger.exception("处理数据时发生异常")   # 等价于 logger.error(..., exc_info=True)

logger.exception() 自动附带堆栈信息。在分布式系统中,还应加上请求 ID、用户 ID 等追踪信息:

try:
    process(user_id, data)
except Exception:
    logger.exception(
        "处理数据失败",
        extra={"user_id": user_id, "trace_id": get_trace_id()}
    )

六、常见的异常处理反模式

反模式 1:捕获过于宽泛后重新抛出

try:
    do_stuff()
except Exception as e:
    raise e   # 丢失了原始堆栈

try:
    do_stuff()
except Exception:
    logger.exception("do_stuff 失败")
    raise       # 不带参数,保留原始堆栈

反模式 2:在循环中反复捕获异常

for item in huge_list:
    try:
        process(item)
    except SomeError:
        continue

valid = [item for item in huge_list if is_valid(item)]
for item in valid:
    process(item)

反模式 3:except 后面不指定异常类型

try:
    run_server()
except:
    rollback()

try:
    run_server()
except Exception:
    rollback()
    raise

收尾

异常处理不是"出了问题再加个 try",而是代码架构的一部分。几条可以立刻用在项目里的实践:

异常分层:业务层定义自己的异常基类,各模块继承细化。

日志先行:任何 except 块都必须写日志,exc_info=True 不要省。

精确捕获:except Exception 是底线,不要裸 except。写具体的异常类型。

用 raise ... from 保留上下文:排查问题时,原始异常往往比包装后的异常更有价值。

别让异常处理影响正常路径的可读性:else 子句和上下文管理器能让代码更清晰。

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

 

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

2420 篇文章

作家榜 »

  1. Pack 2420 文章
  2. 轩辕小不懂 2403 文章
  3. 小柒 2228 文章
  4. Nen 576 文章
  5. 王昭君 216 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章