内置异常的层次

记住几个常见的:

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception            ← 业务代码 catch 的根
    ├── ValueError
    │   └── UnicodeError
    ├── TypeError
    ├── KeyError
    ├── IndexError
    ├── FileNotFoundError
    ├── PermissionError
    ├── ZeroDivisionError
    ├── RuntimeError
    │   ├── RecursionError
    │   └── NotImplementedError
    └── StopIteration

永远 except Exception,不要 except BaseException——后者会吞掉 Ctrl+C。

何时该 raise

  1. 参数不合法(值 / 类型):ValueError / TypeError
  2. 状态错RuntimeError
  3. 业务规则违反:自定义异常
  4. 必须在子类实现NotImplementedError
def transfer(from_acc, to_acc, amount):
    if amount <= 0:
        raise ValueError("金额必须为正")
    if amount > from_acc.balance:
        raise InsufficientFunds(f"账户 {from_acc.id} 余额不足")
    ...

何时该 catch

只 catch 你能处理的——其他放任抛出,让上层决定:

# 好:能处理(给默认值)
def to_int(s):
    try:
        return int(s)
    except ValueError:
        return 0

# 坏:catch 但啥也不做
try:
    do_something()
except Exception:
    pass    # 隐藏 bug

自定义异常类

继承 Exception 即可:

class AppError(Exception):
    """所有业务异常的基类"""

class NotFound(AppError):
    """找不到资源"""

class PermissionDenied(AppError):
    """没权限"""

class InsufficientFunds(AppError):
    def __init__(self, msg, current=0, required=0):
        super().__init__(msg)
        self.current = current
        self.required = required


try:
    transfer(...)
except InsufficientFunds as e:
    print(f"需要 {e.required},账户只有 {e.current}")
except AppError as e:               # 兜底所有业务异常
    print(f"业务错误: {e}")

异常链:from

转换异常时,保留原始 traceback:

def load_config(path):
    try:
        with open(path) as f:
            return json.loads(f.read())
    except FileNotFoundError as e:
        raise ConfigError("配置不存在") from e
    except json.JSONDecodeError as e:
        raise ConfigError("配置格式错") from e

from e 让 traceback 显示"this is the cause of",便于排查。

raise from None:故意隐藏内部细节

try:
    parse_internal()
except InternalError:
    raise UserFacingError("配置错误") from None    # 不暴露内部异常

异常应该是异常

不要用异常做"正常流程控制":

# 反例:用异常控制
def find(x):
    try:
        return DB[x]
    except KeyError:
        return None

# 更地道
def find(x):
    return DB.get(x)

异常的代价比 if 高很多——用在真正"不期望发生"的事情上。

上下文管理器抑制异常

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("maybe.txt")          # 不存在就算了

比写 try: ... except: pass 更优雅。

实战:API 错误处理框架

class APIError(Exception):
    status_code = 500
    message = "服务器错误"

class NotFoundError(APIError):
    status_code = 404
    message = "资源不存在"

class ValidationError(APIError):
    status_code = 400
    message = "请求参数错误"


@app.errorhandler(APIError)
def handle(e):
    return {"error": e.message}, e.status_code

整个项目用一套异常体系,错误处理一处搞定。

下一篇讲日志 logging——为什么 print 不够。