上一篇 异常处理机制 try/except 讲了怎么抓异常。这一篇讲怎么设计 自己的异常——什么时候用内置异常、什么时候造新异常、怎么组织异常层次、怎么携带上下文。写库/框架/中大型项目时,一个”好设计的异常体系”能省下无数调试时间。
一、什么时候自定义异常 先看一个反面例子:
1 2 3 4 5 6 7 8 9 def transfer (from_acct, to_acct, amount ): if amount <= 0 : raise ValueError("金额必须 > 0" ) if amount > from_acct.balance: raise ValueError("余额不足" ) if from_acct.frozen: raise ValueError("账户被冻结" ) ...
调用方看到 ValueError 不知道具体是哪种:
1 2 3 4 try : transfer(a, b, 100 ) except ValueError: ...
自定义异常 能表达更精确的语义:
1 2 3 4 5 6 7 8 9 10 11 12 class BankError (Exception ): ...class InvalidAmount (BankError ): ...class InsufficientFunds (BankError ): ...class AccountFrozen (BankError ): ...def transfer (from_acct, to_acct, amount ): if amount <= 0 : raise InvalidAmount(amount) if amount > from_acct.balance: raise InsufficientFunds(need=amount, have=from_acct.balance) if from_acct.frozen: raise AccountFrozen(from_acct)
调用方:
1 2 3 4 5 6 7 8 try : transfer(a, b, 100 ) except InsufficientFunds as e: show_topup_page() except AccountFrozen: show_frozen_notice() except InvalidAmount: show_amount_help()
二、最简自定义异常 1 2 class MyError (Exception ): pass
一行搞定 。继承 Exception 就好,不用继承 BaseException(那是给 KeyboardInterrupt 用的)。
三、异常携带信息 给异常加参数:
1 2 3 4 5 6 7 8 9 10 11 12 class InsufficientFunds (Exception ): def __init__ (self, need: float , have: float ): self .need = need self .have = have super ().__init__(f"需要 ¥{need} , 只有 ¥{have} " ) try : raise InsufficientFunds(need=100 , have=50 ) except InsufficientFunds as e: print (e.need, e.have) print (str (e))
super().__init__(msg) 让 str(e) 有个友好的字符串;
结构化的字段(need、have)方便调用方按需处理。
四、异常层次设计 给你的项目/库定义一个根异常 ,其他异常都继承它:
1 2 3 4 5 6 7 8 9 10 11 12 13 class MyLibError (Exception ): """MyLib 所有异常的根。""" class ConnectionError (MyLibError ): """网络连接问题。""" class TimeoutError (MyLibError ): """超时问题。""" class ValidationError (MyLibError ): """输入校验失败。""" class InvalidType (ValidationError ): ... class OutOfRange (ValidationError ): ...
调用方一律 except MyLibError 就能把你的库的所有异常兜住,不会误伤别的:
1 2 3 4 5 try : my_lib.do_stuff() except MyLibError as e: log(e) return None
想更精细就 except ConnectionError / except ValidationError;
千万不要跟 Python 内置异常同名 (除非你就是要”包装内置”,那也要谨慎)。
五、经典的异常层次范例 看 requests 库怎么设计的:
1 2 3 4 5 6 7 8 9 10 RequestException ← 所有异常的根 ├── HTTPError ├── ConnectionError │ ├── ProxyError │ └── SSLError ├── Timeout │ ├── ConnectTimeout │ └── ReadTimeout ├── URLRequired └── TooManyRedirects
调用方可以:
except requests.Timeout:所有超时;
except requests.ConnectionError:所有连接问题;
except requests.RequestException:所有 requests 异常。
六、包装底层异常 一个函数不应该”向调用方泄漏内部实现细节”。把底层异常包成业务异常:
1 2 3 4 5 6 7 8 9 10 11 12 import jsonclass ConfigError (Exception ): ...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"配置文件 {path} 格式错误" ) from e
调用方只 except ConfigError 就够,不用关心底层是 file 还是 json 问题。from e 让 traceback 保留原始原因 。
七、”警告”不是异常 有时候你想告诉调用方”这个功能未来会移除”、”这个参数不推荐”,用 warnings 而不是异常:
1 2 3 4 5 import warningsdef old_func (x ): warnings.warn("old_func 已弃用,请用 new_func" , DeprecationWarning) ...
warnings 默认打印到 stderr,可以被过滤/升级为异常。
八、异常 vs 返回错误码 Python 里几乎所有错误都用异常表达,而不是返回 -1 或 False。原因:
强制调用方处理(不写 try 就崩,比”忘检查返回值”明显得多);
保留完整的 traceback;
不占用返回值。
返回错误码的少数场景 :
处理”预期的失败”且频率高(例如 dict.get(k, default) 找不到 key);
性能极端敏感的循环(try/except 有一点开销)。
九、命名约定
异常类名以 Error 或 Exception 结尾 :ValueError、TimeoutError;
约定俗成有的名字 别乱用(AttributeError、KeyError 等);
一般项目的根异常可以叫 MyProjectError。
十、异常与 dataclass Python 3.7+ 可以用 dataclass 让异常代码更干净,但 dataclass 生成的 __init__ 会覆盖 Exception 的初始化,需要额外注意:
1 2 3 4 5 6 7 8 9 10 from dataclasses import dataclass@dataclass class InsufficientFunds (Exception ): need: float have: float def __str__ (self ): return f"需要 ¥{self.need} , 只有 ¥{self.have} "
十一、异常的性能 Python 的异常只在被抛出时才有开销 ——正常路径几乎零成本。所以 EAFP 风格并不慢:
1 2 3 4 5 try : return d[key] except KeyError: return default
但在循环里高频抛异常 就慢了:
1 2 3 4 5 6 7 8 9 10 11 12 13 for k in keys: try : v = d[k] process(v) except KeyError: pass for k in keys: v = d.get(k) if v is not None : process(v)
十二、日志与异常 打异常日志的正确姿势:
1 2 3 4 5 6 7 8 import logginglogger = logging.getLogger(__name__) try : do_stuff() except Exception: logger.exception("do_stuff 失败" ) raise
logger.exception 会自动把当前异常的完整 traceback 记进日志,比 logger.error(str(e)) 有用得多。
十三、context manager 里的异常 见 with 语句与上下文管理器 。简而言之:__exit__ 里返回 True 会吞掉异常(一般别这么做)。
十四、一个完整例子:文件处理库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 """文件处理库,展示异常层次设计。""" import jsonclass FileProcError (Exception ): """所有异常的根。""" class InvalidInput (FileProcError ): """输入不合法。""" class ParseError (FileProcError ): """解析失败。""" def __init__ (self, msg, path, line=None ): self .path = path self .line = line super ().__init__(f"{msg} (file={path} , line={line} )" ) class WriteError (FileProcError ): """写入失败。""" def load_json (path: str ) -> dict : if not path.endswith(".json" ): raise InvalidInput(f"{path} 不是 .json 文件" ) try : with open (path, encoding="utf-8" ) as f: return json.load(f) except FileNotFoundError as e: raise ParseError("文件不存在" , path) from e except json.JSONDecodeError as e: raise ParseError(e.msg, path, e.lineno) from e def save_json (path: str , data: dict ) -> None : try : with open (path, "w" , encoding="utf-8" ) as f: json.dump(data, f, ensure_ascii=False , indent=2 ) except OSError as e: raise WriteError(f"写 {path} 失败:{e} " ) from e if __name__ == "__main__" : try : data = load_json("config.json" ) data["updated" ] = True save_json("config.json" , data) except InvalidInput as e: print (f"[输入错误] {e} " ) except ParseError as e: print (f"[解析错误] {e.path} :{e.line} - {e} " ) except WriteError as e: print (f"[写入错误] {e} " ) except FileProcError as e: print (f"[未知错误] {e} " )
十五、常见陷阱
异常吞掉不打日志 :except: pass 让 bug 无处可查。
重新抛异常时改成 raise e :会丢失原 traceback,要 raise 或 raise ... from e。
__init__ 里忘 super().__init__(msg) :str(e) 会是空的。
在自定义异常里存太多状态 :异常应该是”信号”,不要变成通用容器。
抛异常代替返回值 :控制流用异常是反模式,正常业务分支还是用返回值。
异常翻译过多层 :层数太多 traceback 冗长且难追。一层包装够了。
异常里带敏感信息 :raise Error(f"password={pwd}") 会被记进日志。
十六、小结与延伸阅读
简单的 class MyError(Exception): pass 就能造异常;
给项目建立一个根异常,所有内部异常继承它;
包装底层异常时用 from;
异常应表达”是什么错”,不是”返回的错误码”;
打日志用 logger.exception;
命名以 Error / Exception 结尾;
别把异常当控制流用。
延伸阅读:
下一篇 文件读写操作 我们讲怎么读写文件,异常和 with 一起用。