try-except 语句
try-except 是 Python 处理异常的核心语法。它让你能把"可能出错的代码"和"出错后的处理逻辑"清晰地区分开来,从而写出既健壮又易读的程序。本节将系统介绍 try-except-else-finally 的完整结构、多异常捕获、异常传递与链式抛出,以及 Python 3.11+ 引入的 ExceptionGroup 与 except* 语法。
基本结构
最简单的形式:把可能抛出异常的代码放进 try 块,后面跟一个或多个 except 子句捕获并处理异常。
try:
number = int(input("请输入一个数字:"))
print(f"你输入的数字是 {number}")
except ValueError:
print("输入的不是有效数字!")
执行流程:
- 进入
try块执行 - 如果没抛异常,跳过所有
except,继续后面的代码 - 如果抛出异常且类型匹配某个
except,执行对应的处理代码 - 如果抛出异常但没有匹配的
except,异常继续向上传播
:::tip except 必须匹配异常类型
except ValueError 只能捕获 ValueError 及其子类。捕获范围之外的异常会照常向上传播。捕获过宽(如 except BaseException)会掩盖意外错误,应避免。
:::
as 绑定异常对象
用 as 把异常对象绑定到变量,可以读取异常信息或调用其方法:
try:
value = int("not a number")
except ValueError as exc:
print(f"捕获到异常:{exc}")
print(f"异常类型:{type(exc).__name__}")
print(f"参数列表:{exc.args}")
# 输出:
# 捕获到异常:invalid literal for int() with base 10: 'not a number'
# 异常类型:ValueError
# 参数列表:("invalid literal for int() with base 10: 'not a number'",)
:::info 异常变量的作用域
Python 3 中,except ... as exc 绑定的 exc 在 except 块结束时会被自动删除,避免循环引用导致的内存泄漏。这意味着你不能在 except 块外面再使用 exc:
try:
1 / 0
except ZeroDivisionError as exc:
pass
print(exc) # NameError: name 'exc' is not defined
如果需要在 except 块外使用,可以提前赋值给一个外部变量。
:::
捕获多种异常
多个 except 子句
把不同异常的处理逻辑分开写,便于针对性处理:
def parse_and_double(text: str):
try:
number = int(text)
return number * 2
except ValueError:
print("输入不是整数")
return None
except TypeError:
print("输入类型不对,需要字符串")
return None
print(parse_and_double("21")) # 输出:42
print(parse_and_double("abc")) # 输出:输入不是整数 / None
print(parse_and_double(None)) # 输出:输入类型不对,需要字符串 / None
一个 except 捕获多种异常
把多个异常类型放进元组,共用同一段处理逻辑:
def safe_get(mapping, key):
try:
return mapping[key]
except (KeyError, TypeError, IndexError) as exc:
print(f"获取失败:{exc}")
return None
print(safe_get({"a": 1}, "b")) # 输出:获取失败:'b' / None
print(safe_get(None, "x")) # 输出:获取失败:'NoneType' object is not subscriptable / None
:::warning 元组语法陷阱
except (A, B): 中的异常类型必须放在元组里。如果写成 except A, B:(Python 2 的语法),在 Python 3 中会报语法错误。如果是单个异常,可以不写元组:except ValueError:。
:::
捕获顺序
except 子句按从上到下的顺序匹配,先匹配更具体的异常。如果父类异常在前,子类异常的 except 永远不会被触发:
try:
raise KeyError("missing")
except LookupError: # KeyError 是 LookupError 的子类,会先匹配这里
print("被 LookupError 捕获了") # 输出这行
except KeyError: # 这里永远不会执行(dead code)
print("被 KeyError 捕获了")
正确做法是把更具体的异常放前面:
try:
raise KeyError("missing")
except KeyError: # 先匹配具体类型
print("被 KeyError 捕获了")
except LookupError: # 兜底
print("被 LookupError 捕获了")
兜底捕获 Exception
如果需要捕获所有"普通异常"作为兜底,用 except Exception:
def robust_operation():
try:
do_something_risky()
except ValueError as e:
print(f"值错误:{e}")
except KeyError as e:
print(f"键错误:{e}")
except Exception as e: # 兜底,捕获所有其他 Exception 子类
print(f"未预期的错误:{type(e).__name__}: {e}")
raise # 重新抛出,让上层也知晓
:::warning 永远不要用 except: 裸捕获
绝对不要写裸的 except: 或 except BaseException:,这会同时捕获 KeyboardInterrupt、SystemExit 等系统信号,导致 Ctrl+C 都无法中断程序。如果确实要兜底,用 except Exception:。
:::
else 子句
else 子句只在 try 块没有抛出异常时执行。它适合放"成功后的处理逻辑",让 try 块只包含真正可能抛异常的代码——这样能避免捕获到本不该捕获的异常。
def read_number(prompt: str) -> int | None:
try:
text = input(prompt)
number = int(text)
except ValueError:
print("不是有效数字")
return None
else:
# try 块成功完成才执行这里
print(f"解析成功:{number}")
return number
为什么需要 else
考虑下面的反面例子——把后续逻辑也放进 try 块:
# 不推荐:把后续逻辑也塞进 try 块
try:
text = input("请输入:")
number = int(text)
result = 100 / number # 这里也可能抛 ZeroDivisionError
print(f"100 / {number} = {result}")
except ValueError:
print("输入不是数字")
# 问题:result 的 ZeroDivisionError 也会被这个 except 捕获,
# 但它本不是我们想处理的!
正确做法是用 else 隔离"可能抛 ValueError 的部分"和"成功后的后续逻辑":
# 推荐:用 else 隔离
try:
number = int(input("请输入:"))
except ValueError:
print("输入不是数字")
else:
# 只有解析成功才执行
result = 100 / number
print(f"100 / {number} = {result}")
# 如果 number 为 0,这里的 ZeroDivisionError 会向上传播,不会被误捕获
:::tip try 块要尽量"瘦"
最佳实践:try 块中只放真正可能抛出你想捕获的异常的代码,其他逻辑放到 else 里。这样能避免意外捕获到不该处理的异常,也让代码意图更清晰。
:::
finally 子句
finally 子句无论是否抛出异常都会执行,通常用于资源清理(关闭文件、释放锁、断开连接)。
def read_file_safely(path: str) -> str | None:
f = open(path)
try:
return f.read()
except OSError as e:
print(f"读取失败:{e}")
return None
finally:
f.close() # 无论成功失败都关闭文件
print("文件已关闭")
finally 的关键特性
- 始终执行:即使 try 块里有
return、break、continue,finally仍会执行 - 吞异常的陷阱:
finally中的return或抛出新异常会覆盖原异常 - 异常重新抛出前执行:异常未被 except 捕获时,
finally仍会先执行
def demo_return_in_finally():
try:
return "from try"
finally:
return "from finally" # ← 这会覆盖 try 的返回值!
print(demo_return_in_finally()) # 输出:from finally
:::warning 不要在 finally 里 return
finally 块里写 return 是一个隐蔽的 bug 来源——它会覆盖 try 块的返回值,甚至会吞掉 try 块中抛出的异常。finally 应该只用于"清理",不要用于"返回结果"。
:::
完整结构
try-except-else-finally 的完整形式:
def divide(a, b):
try:
result = a / b
except ZeroDivisionError as e:
print(f"除零错误:{e}")
return None
except TypeError as e:
print(f"类型错误:{e}")
return None
else:
print(f"计算成功:{a}/{b} = {result}")
return result
finally:
print("运算结束")
print(divide(10, 2))
# 输出:
# 计算成功:10/2 = 5.0
# 运算结束
# 5.0
print(divide(10, 0))
# 输出:
# 除零错误:division by zero
# 运算结束
# None
执行顺序总结
| 情况 | 执行顺序 |
|---|---|
| try 无异常 | try → else → finally |
| try 抛异常且被捕获 | try → except → finally |
| try 抛异常未被捕获 | try → finally(异常继续传播) |
| try 中 return | return 前先执行 finally |
raise 重新抛出
except 块里用裸 raise 可以重新抛出当前捕获的异常,保留完整的原始 Traceback:
def load_config(path: str) -> dict:
try:
with open(path) as f:
text = f.read()
except FileNotFoundError:
print(f"配置文件 {path} 不存在,使用默认配置")
raise # 让上层调用者也知道这件事
try:
config = load_config("missing.conf")
except FileNotFoundError:
print("上层也处理了这个错误")
部分处理后继续抛出
可以在 except 中做"日志记录"或"局部恢复",然后重新抛出让上层做最终决策:
import logging
logger = logging.getLogger(__name__)
def fetch_user(user_id: int) -> dict:
try:
response = call_api(user_id) # 假设会抛 ConnectionError
except ConnectionError as e:
logger.warning("API 调用失败,可能是网络问题:%s", e)
raise # 让上层决定是否重试或显示错误页
return response["data"]
raise from 链式异常
有时你需要在 except 块中抛出另一种异常,但希望保留原始异常作为"上下文"。Python 提供两种链式异常机制:
raise X from Y:显式链式(__cause__),表示"由 Y 导致了 X"raise X(在 except 块中):隐式链式(__context__),表示"在处理 X 时又发生了 X"
显式链式:raise ... from ...
def load_json_config(path: str) -> dict:
try:
with open(path, encoding="utf-8") as f:
text = f.read()
except OSError as e:
# 把 OSError 转成更友好的自定义异常,并保留原始信息
raise RuntimeError(f"无法读取配置文件 {path}") from e
try:
config = load_json_config("missing.conf")
except RuntimeError as exc:
print(f"主异常:{exc}")
print(f"原因:{exc.__cause__}")
print(f"原因类型:{type(exc.__cause__).__name__}")
# 输出:
# 主异常:无法读取配置文件 missing.conf
# 原因:[Errno 2] No such file or directory: 'missing.conf'
# 原因类型:FileNotFoundError
打印 Traceback 时,Python 会显示:
...
OSError: [Errno 2] No such file or directory: 'missing.conf'
The above exception was the direct cause of the following exception:
...
RuntimeError: 无法读取配置文件 missing.conf
隐式链式:except 块内 raise
如果在 except 块里抛出新异常而不写 from,Python 也会自动把原异常作为上下文保存:
def parse_strict(text: str) -> int:
try:
return int(text)
except ValueError:
# 不写 from,但 Python 仍保留原 ValueError 作为 __context__
raise RuntimeError("输入数据格式不正确")
try:
parse_strict("abc")
except RuntimeError as exc:
print(f"主异常:{exc}")
print(f"上下文:{exc.__context__}")
print(f"上下文类型:{type(exc.__context__).__name__}")
# 输出:
# 主异常:输入数据格式不正确
# 上下文:invalid literal for int() with base 10: 'abc'
# 上下文类型:ValueError
Traceback 中显示:
ValueError: invalid literal for int() with base 10: 'abc'
During handling of the above exception, another exception occurred:
...
RuntimeError: 输入数据格式不正确
:::tip 显式 vs 隐式
- 写
raise X from Y时显示 "The above exception was the direct cause of..." - 不写
from时显示 "During handling of the above exception, another exception occurred:"
显式 from 表达"故意转换"的语义更清晰。如果是有意把低层异常转换成高层业务异常,用 raise ... from ...;如果是 except 块内部意外出错,保留隐式上下文即可。
:::
抑制链式:raise ... from None
如果你确定不希望暴露原始异常(例如出于安全或简洁考虑),可以用 from None 抑制链:
def parse_secret(token: str) -> dict:
try:
# 解析逻辑可能抛多种异常
return _internal_parse(token)
except Exception as e:
# 只暴露友好信息,不暴露内部细节
raise ValueError("无效的 token") from None
Traceback 中将只显示 ValueError: 无效的 token,不附带原始异常。谨慎使用 from None——大多数情况下保留链式信息有助于调试。
不捕获 BaseException
BaseException 是所有异常的根类,包含了 KeyboardInterrupt(用户按 Ctrl+C)、SystemExit(sys.exit() 触发)、GeneratorExit(生成器关闭)等"非错误"信号。捕获它会带来严重问题:
# ❌ 反例:捕获 BaseException 会让 Ctrl+C 失效
import time
try:
while True:
time.sleep(1)
print("运行中...")
except BaseException: # Ctrl+C 会被这里吞掉,程序停不下来!
print("捕获了某些异常")
:::warning 为什么不要捕获 BaseException
KeyboardInterrupt:用户主动中断,应让程序立即停止SystemExit:sys.exit()触发,应让程序正常退出GeneratorExit:生成器被关闭的内部信号
如果用 except BaseException 或裸 except: 捕获它们,这些信号会变成普通异常被处理,程序无法被正常中断。永远只用 except Exception(或更具体的异常类型)兜底。
:::
正确做法:
# ✅ 正例:用 Exception 兜底,KeyboardInterrupt 仍能正常工作
try:
while True:
time.sleep(1)
print("运行中...")
except Exception as e:
print(f"捕获到错误:{e}")
# 按 Ctrl+C 仍能正常中断程序(抛出 KeyboardInterrupt,未被捕获)
Python 3.11+ ExceptionGroup
Python 3.11 引入了 ExceptionGroup(异常组)和 except* 语法,专门处理"一次操作可能同时抛出多个异常"的场景,典型应用是并发任务(asyncio.TaskGroup、concurrent.futures)。
为什么需要异常组
传统 raise 一次只能抛一个异常。但在并发场景下,多个任务可能同时失败:
import asyncio
async def task_a():
raise ValueError("任务 A 失败")
async def task_b():
raise KeyError("任务 B 失败")
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(task_a())
tg.create_task(task_b())
# TaskGroup 会等所有任务结束,把所有异常打包成 ExceptionGroup 抛出
asyncio.run(main())
执行后会看到:
+ Exception Group Traceback (most recent call last):
| File "...", line X, in main
| async with asyncio.TaskGroup() as tg:
| ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| ExceptionGroup: 2 exceptions were raised (2 sub-exceptions)
+-+---------------- 1 ----------------
| ValueError: 任务 A 失败
+---------------- 2 ----------------
| KeyError: 任务 B 失败
+------------------------------------
except* 语法
普通 except 无法拆解 ExceptionGroup 内部的多个异常。Python 3.11 引入 except* 语法,能按类型分别处理组中的每个异常:
def run_concurrent():
try:
# 假设 run_all_tasks() 会抛 ExceptionGroup
run_all_tasks()
except* ValueError as eg:
# eg 是 ExceptionGroup,包含所有 ValueError 子异常
print(f"处理了 {len(eg.exceptions)} 个 ValueError")
for exc in eg.exceptions:
print(f" - {exc}")
except* KeyError as eg:
print(f"处理了 {len(eg.exceptions)} 个 KeyError")
for exc in eg.exceptions:
print(f" - {exc}")
手动创建 ExceptionGroup
也可以手动构造 ExceptionGroup:
errors = [
ValueError("第一个值错误"),
TypeError("类型错误"),
ValueError("第二个值错误"),
]
try:
raise ExceptionGroup("批量操作失败", errors)
except* ValueError as eg:
print(f"捕获到 {len(eg.exceptions)} 个 ValueError:")
for exc in eg.exceptions:
print(f" - {exc}")
except* TypeError as eg:
print(f"捕获到 {len(eg.exceptions)} 个 TypeError:")
for exc in eg.exceptions:
print(f" - {exc}")
# 输出:
# 捕获到 2 个 ValueError:
# - 第一个值错误
# - 第二个值错误
# 捕获到 1 个 TypeError:
# - 类型错误
:::info except vs except*
except ValueError:只能匹配单个ValueError实例,不匹配ExceptionGroupexcept* ValueError:会拆开ExceptionGroup,把其中所有ValueError子异常收集起来交给处理代码- 如果
ExceptionGroup中有未被任何except*匹配的异常,会重新打包成新的ExceptionGroup继续向上抛
except* 是 Python 3.11+ 才有的语法,旧版本会报语法错误。
:::
except* 与部分处理
except* 允许"部分处理"——只处理一部分异常,未处理的会被重新打包抛出:
def handle_batch():
try:
raise ExceptionGroup("操作失败", [
ValueError("值错误 1"),
ValueError("值错误 2"),
KeyError("键错误"),
TypeError("类型错误"),
])
except* ValueError as eg:
# 只处理 ValueError,其他的继续传播
print(f"处理了 {len(eg.exceptions)} 个 ValueError")
# KeyError 和 TypeError 会被重新打包成 ExceptionGroup 继续抛
:::tip ExceptionGroup 不可拆解
ExceptionGroup 本身不能被普通 except 捕获(除了 except ExceptionGroup: 或 except BaseException)。这是设计有意为之——它强迫你用 except* 逐一处理组内异常,避免误吞部分异常。这也是为什么传统代码遇到 ExceptionGroup 会直接崩溃:需要专门处理。
:::
实战:健壮的输入处理
下面实现一个交互式的"安全计算器",综合运用 try-except-else-finally、多异常捕获、链式异常等技巧:
from __future__ import annotations
class CalculatorError(Exception):
"""计算器所有错误的基类"""
def parse_number(text: str) -> float:
"""把字符串解析成数字,转换内部异常为业务异常"""
try:
return float(text)
except ValueError as exc:
raise CalculatorError(f"无法解析为数字:{text!r}") from exc
def safe_divide(a: float, b: float) -> float:
"""安全的除法,处理除零"""
try:
return a / b
except ZeroDivisionError as exc:
raise CalculatorError("除数不能为零") from exc
def calculator():
"""交互式计算器,演示健壮的输入处理"""
print("简易计算器(输入 q 退出)")
print("支持:a + b、a - b、a * b、a / b")
while True:
try:
user_input = input("\n> ").strip()
except (EOFError, KeyboardInterrupt):
# Ctrl+D 或 Ctrl+C:优雅退出
print("\n再见!")
break
if not user_input:
continue
if user_input.lower() in ("q", "quit", "exit"):
print("再见!")
break
# 解析输入:a op b
parts = user_input.split()
if len(parts) != 3:
print("格式错误,请输入:a op b(如 3 + 5)")
continue
a_str, op, b_str = parts
try:
a = parse_number(a_str)
b = parse_number(b_str)
except CalculatorError as exc:
print(f"输入错误:{exc}")
continue
# 执行运算
try:
if op == "+":
result = a + b
elif op == "-":
result = a - b
elif op == "*":
result = a * b
elif op == "/":
result = safe_divide(a, b)
else:
print(f"不支持的运算符:{op}")
continue
except CalculatorError as exc:
# 业务异常:已知错误,给出友好提示
print(f"运算错误:{exc}")
continue
except Exception as exc:
# 兜底:未预期错误,记录后继续运行
print(f"未预期错误:{type(exc).__name__}: {exc}")
continue
else:
print(f"结果:{result}")
finally:
# 无论成功失败,记录这次操作的统计
pass
# 非交互模式下的批量处理示例
def batch_calculate(expressions: list[str]) -> list[float | str]:
"""批量计算表达式,每个失败的返回错误信息字符串"""
results: list[float | str] = []
for expr in expressions:
try:
parts = expr.split()
if len(parts) != 3:
raise CalculatorError(f"格式错误:{expr!r}")
a = parse_number(parts[0])
b = parse_number(parts[2])
op = parts[1]
if op == "+":
results.append(a + b)
elif op == "-":
results.append(a - b)
elif op == "*":
results.append(a * b)
elif op == "/":
results.append(safe_divide(a, b))
else:
raise CalculatorError(f"不支持的运算符:{op}")
except CalculatorError as exc:
results.append(f"错误:{exc}")
except Exception as exc:
# 兜底,把未知错误也转成 CalculatorError
results.append(f"未知错误:{type(exc).__name__}: {exc}")
return results
# 演示批量处理
if __name__ == "__main__":
test_cases = [
"3 + 5",
"10 / 0", # 除零
"abc + 2", # 解析失败
"8 * 7",
"100 / 4",
"5 ^ 2", # 不支持的运算符
"9 - 3",
]
print("批量计算结果:")
for expr, result in zip(test_cases, batch_calculate(test_cases)):
print(f" {expr:<12} → {result}")
输出示例:
批量计算结果:
3 + 5 → 8.0
10 / 0 → 错误:除数不能为零
abc + 2 → 错误:无法解析为数字:'abc'
8 * 7 → 56.0
100 / 4 → 25.0
5 ^ 2 → 错误:不支持的运算符:^
9 - 3 → 6.0
:::tip 设计模式:内部异常 + 外部异常
上面的代码示范了一个常见模式:内部用 raise ... from ... 把底层异常(ValueError、ZeroDivisionError)转换成统一的业务异常 CalculatorError,对外只暴露友好信息,同时保留原始异常作为 __cause__ 供调试。这让 API 调用者只需关心一种异常类型,简化错误处理代码。
:::
小结
try-except-else-finally是完整的异常处理结构:try:放可能抛异常的代码(尽量"瘦")except:捕获并处理特定异常else:try 成功后才执行的逻辑(避免误捕获后续异常)finally:无论成功失败都执行的清理逻辑
- 用
as绑定异常对象,便于读取错误信息 - 多种异常可以放进元组:
except (A, B, C): - 捕获顺序:先具体后宽泛,父类异常放在子类之后
- 永远不要用裸
except:或except BaseException:,用except Exception:兜底 raise重新抛出当前异常,保留原始 Tracebackraise X from Y显式链式异常,保留__cause__;raise X from None抑制链- Python 3.11+ 引入
ExceptionGroup+except*处理并发场景下的多个异常 - 实战模式:用
raise ... from ...把底层异常转换为统一的业务异常,对外简化接口
下一节我们将学习如何设计自定义异常类,构建清晰的业务错误体系。