异常处理机制 try/except

程序不可能”永远走 happy path”。用户会输入奇怪的数据、网络会断、文件会不存在、磁盘会满、依赖会挂——这些都会让程序抛出异常。Python 用 try/except 处理异常,规则简单,但用好并不容易。这一篇讲清楚异常的语法、几种典型模式、”要 catch 还是让它挂”的判断、以及 Python 3.11+ 的一些新语法。

一、什么是异常

异常是”运行时出错时抛出的对象”:

1
2
3
4
5
6
7
8
1 / 0
# ZeroDivisionError: division by zero

int("abc")
# ValueError: invalid literal for int() with base 10: 'abc'

open("no-such-file.txt")
# FileNotFoundError: ...

Python 里所有异常都是 BaseException 的子类。日常业务代码几乎只 catch Exception 及其子类。

二、基本语法

1
2
3
4
5
6
7
8
9
10
11
12
try:
risky_operation()
except ValueError:
handle_value_error()
except (FileNotFoundError, PermissionError) as e:
print(f"文件问题:{e}")
except Exception as e:
print(f"其他错误:{e}")
else:
print("try 里没抛异常时执行")
finally:
print("无论如何都执行(清理资源)")

执行顺序

  1. 运行 try 块;
  2. 如果抛异常 → 从上到下匹配 except;
  3. 如果没抛异常 → 执行 else;
  4. 无论走哪条路 → 执行 finally。

三、抓具体异常,不要裸 except

1
2
3
4
5
# ❌ 万恶之源
try:
do_stuff()
except:
pass # 什么错都吞掉,KeyboardInterrupt 也吞

问题:

  • 遮蔽真正的 bug;
  • 屏蔽 Ctrl+C(因为 KeyboardInterrupt 也是异常);
  • 让调试变噩梦。

规则

  • 尽量抓具体的异常(except ValueError);
  • 不确定时 except Exception(不会吞 KeyboardInterrupt/SystemExit);
  • 永远不写裸 except:

四、只 catch 你能处理的

能处理的意思是”知道怎么办”——重试、返回默认值、给用户友好提示、清理状态。如果你只是想”让程序不崩”,那不是处理,那是掩盖 bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ❌ 反面:只是想不崩
try:
result = fetch_from_api()
except Exception:
result = None # 上层拿到 None 不知所措

# ✅ 正面:有明确处理策略
try:
result = fetch_from_api()
except NetworkError:
result = read_from_cache() # 网络挂就用缓存
except TimeoutError:
result = None
logger.warning("超时,返回 None")

五、抛异常:raise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def divide(a, b):
if b == 0:
raise ValueError("除数不能为 0")
return a / b

# 重新抛
try:
do_stuff()
except ValueError:
logger.error("出错")
raise # 保留原始 traceback

# 抛新异常但保留原因
try:
do_stuff()
except ValueError as e:
raise RuntimeError("上层错误") from e

raise ... from e 让 traceback 显示”是从这个原因引发的”,调试非常清晰。

六、常见内置异常

异常 意思
ValueError 值不对(例:int("abc")
TypeError 类型不对(例:"a" + 1
KeyError 字典 key 不存在
IndexError 列表索引越界
AttributeError 属性不存在
NameError 变量不存在
FileNotFoundError 文件不存在
PermissionError 权限不足
IOError / OSError IO / 系统错误
ZeroDivisionError 除零
TimeoutError 超时
StopIteration 迭代结束
KeyboardInterrupt Ctrl+C
RuntimeError 通用运行时错误
NotImplementedError 抽象方法未实现
AssertionError assert 失败

七、异常的继承结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception ← 日常抓这个
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
│ ├── FileNotFoundError
│ └── PermissionError
├── ValueError
├── TypeError
└── ...

推论except LookupError 能同时抓 KeyErrorIndexErrorexcept OSError 能抓所有 IO 相关。

八、EAFP vs LBYL

Python 的哲学是 “Easier to Ask Forgiveness than Permission”(EAFP)——先做,出错再处理:

1
2
3
4
5
6
7
8
9
10
11
# ✅ EAFP(Pythonic)
try:
value = d[key]
except KeyError:
value = default

# ❌ LBYL(Look Before You Leap,Java 风格)
if key in d:
value = d[key]
else:
value = default

好处:

  • EAFP 只在异常路径慢,正常路径快;
  • 避免 TOCTOU 问题(检查 → 使用之间状态变了);
  • 代码更直接。

但也不是所有场景都 EAFP。当”检查便宜且大概率失败”时 LBYL 更好

1
2
3
4
if not items:
return # LBYL:空判断很便宜

# 而不是 items[0] 抛 IndexError 再 catch

九、finally:清理资源

finally 无论有没有异常都会执行,适合清理:

1
2
3
4
5
f = open("file.txt")
try:
process(f)
finally:
f.close() # 保证关闭

更 Pythonic 的做法是用 with

1
2
with open("file.txt") as f:
process(f) # 自动关闭

with 用的是上下文管理器 的机制。

十、else 子句

try/else 用得少,但语义清晰:

1
2
3
4
5
6
7
try:
result = do_stuff()
except NetworkError:
result = None
else:
# 只在 do_stuff 没抛异常时执行
log("成功")

规则:把不该抛异常的代码放 else,try 里只放”可能抛”的那句

十一、异常链:raise ... from

想”包装”底层异常成业务异常,同时保留 traceback:

1
2
3
4
5
6
7
8
9
class DatabaseError(Exception):
pass


def query(sql):
try:
return raw_query(sql)
except IOError as e:
raise DatabaseError("查询失败") from e

调用方看到:

1
2
3
4
5
IOError: connection reset

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

DatabaseError: 查询失败

一眼就能看到”表面上抛 DatabaseError,其实底层是 IOError”。

raise ... from None 可以隐藏原因,避免长链暴露实现细节。

十二、Python 3.11+ 的新语法

1. except*:异常组

Python 3.11 引入了 ExceptionGroup,用来同时处理多个异常(并发场景很有用):

1
2
3
4
5
6
7
try:
...
except* ValueError as eg:
for e in eg.exceptions:
print("值错误:", e)
except* TypeError as eg:
...

普通业务不用管,遇到 asyncio.TaskGroup 会用到。

2. add_note

异常可以携带额外备注:

1
2
3
4
5
try:
do_stuff()
except Exception as e:
e.add_note(f"user_id={uid}")
raise

日志里会显示这条备注,无需重新包装异常。

十三、assert:不是异常处理

1
assert x > 0, "x 必须为正"

assert 用来校验假设——不该出现的情况。别用它做业务校验!因为 Python 加 -O 参数就会跳过所有 assert。

规则

  • 输入验证 → raise ValueError
  • 内部不变量 → assert
  • 生产环境的关键校验 → if not ...: raise

十四、常见异常模式

1. 重试

1
2
3
4
5
6
7
8
9
10
11
import time

def with_retry(func, times=3, delay=1):
for i in range(times):
try:
return func()
except Exception as e:
if i == times - 1:
raise
print(f"重试 {i+1}/{times}{e}")
time.sleep(delay)

2. 默认值

1
2
3
4
5
def get_config(key, default=None):
try:
return config[key]
except KeyError:
return default

3. 转换异常

1
2
3
4
try:
x = int(user_input)
except ValueError:
raise BadRequest("请输入数字") from None

4. 日志 + 重抛

1
2
3
4
5
try:
do_stuff()
except Exception:
logger.exception("do_stuff 失败") # 自动记录 traceback
raise

logger.exception 是日志里最有用的方法之一——自动包含完整 traceback。

十五、一个完整例子:健壮的输入读取

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
# safe_input.py
"""带异常处理的输入函数集合。"""

import logging
logger = logging.getLogger(__name__)


def read_int(prompt: str, min_val: int = None, max_val: int = None) -> int:
while True:
raw = input(prompt).strip()
try:
n = int(raw)
except ValueError:
print("请输入整数")
continue

if min_val is not None and n < min_val:
print(f"必须 >= {min_val}")
continue
if max_val is not None and n > max_val:
print(f"必须 <= {max_val}")
continue
return n


def safe_divide(a, b):
"""安全除法。"""
try:
return a / b
except ZeroDivisionError:
logger.warning(f"除零:{a}/{b}")
return float("inf") if a > 0 else float("-inf")
except TypeError as e:
raise TypeError(f"不能除:{a!r} / {b!r}") from e


if __name__ == "__main__":
age = read_int("年龄(1-150):", min_val=1, max_val=150)
print(f"你 {age} 岁")

print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # inf
try:
safe_divide("a", "b")
except TypeError as e:
print("捕获:", e)

十六、常见陷阱

  1. except::吞掉一切,包括 Ctrl+C。永远不要写。
  2. except Exception: pass:跟裸 except 差不多糟,除非你真的知道自己在干嘛。
  3. except Exception as e: raise e:丢失原始 traceback。要 raise(不带 e)。
  4. 在 try 里放太多代码:只把”可能抛”的一句放 try,其他放 else。
  5. assert 做业务校验-O 参数下失效。用 raise
  6. 异常链爆炸:无脑 raise ... from e 让 traceback 特别长。有时用 from None 隐藏。
  7. finally 里 return:会覆盖 try/except 里的 return,谨慎使用。

十七、小结与延伸阅读

  • try/except/else/finally 四件套;
  • 抓具体异常,别裸 except;
  • Python 提倡 EAFP:先做,出错再处理;
  • 清理资源优先用 with,其次 finally
  • raise ... from e 保留原因,raise ... from None 隐藏;
  • assert 用于内部假设,不做业务校验;
  • 日志记异常用 logger.exception

延伸阅读:

下一篇 自定义异常与最佳实践 我们讲怎么设计自己的异常层次。