第6章 · 异常与错误处理(Exceptions)— 现代风格 · 零基础友好¶
目标:学会在 Python 中优雅地处理错误,保护程序稳定性。
风格:所有无序列表使用-(横线),*仅用于加粗;每个知识点都有解释 + 代码 + 调用演示。
1.什么是“异常”?为什么不用返回码?¶
- 异常(Exception):运行时出现错误时,由解释器或你的代码抛出的对象;如果没人处理就会冒泡并终止程序,附带栈回溯信息。
- 对比返回码:返回码常被忽略或被覆盖;异常会强制你关注异常路径,并能携带完整上下文(类型、消息、堆栈)。
- Python 中的异常都是类,大都继承自
Exception。
def divide(a, b):
return a / b
# === 调用演示 ===
# divide(10, 0) # 取消注释将触发 ZeroDivisionError 并终止程序
2.基本语法¶
try放可能出错的代码块。except按类型捕获异常。else在 未发生异常 时执行(可放“成功路径”)。finally无论是否异常都执行(资源清理)。
def parse_int(text: str):
try:
value = int(text) # 可能 ValueError
except ValueError as e:
return f"不是整数:{e}"
else:
return value # 成功时走这里
finally:
pass # 这里通常做清理/关闭资源
# === 调用演示 ===
print(parse_int("123")) # -> 123
print(parse_int("12x")) # -> 不是整数:invalid literal for int() with base 10: '12x'
3. 捕获方式¶
- 具体类型优先:只捕获你确实能处理的异常类型。
- 多异常:
except (TypeError, ValueError) as e:。 as e可拿到异常对象,便于记录/拼装新信息。
def safe_calc(a, b):
try:
return a / b
except (TypeError, ZeroDivisionError) as e:
return f"计算失败:{type(e).__name__} - {e}"
print(safe_calc(10, 2)) # -> 5.0
print(safe_calc(10, 0)) # -> 计算失败:ZeroDivisionError - division by zero
print(safe_calc("10", 2)) # -> 计算失败:TypeError - unsupported operand type(s) for /: 'str' and 'int'
反模式:裸 except
- 禁止
except:或except Exception:兜底一切,容易吞掉编程错误。 - 如果必须兜底,请:日志记录 + 重新抛出或返回明确错误。
4. 主动抛出:raise、二次抛出与异常链¶
- 主动抛出:当检测到“不满足前置条件”时,用
raise明确失败。 - 保留栈重新抛出:裸
raise在except中使用,保留原始堆栈。 - 异常链:
raise NewError(...) from e,把上下游错误关联起来。
def require_positive(n):
if n <= 0:
raise ValueError("n 必须为正数")
def wrapper(n):
try:
require_positive(n)
except ValueError as e:
# 添加业务上下文,并保留原始异常链
raise RuntimeError(f"参数校验失败:n={n}") from e
# === 调用演示 ===
try:
wrapper(-1)
except Exception as e:
print(type(e).__name__, "->", e.__cause__) # -> RuntimeError -> ValueError('n 必须为正数')
5. 自定义异常:分层设计,让错误可读可判¶
- 继承
Exception(或某个标准异常)创建语义清晰的异常层级。 - 好处:
except MyBaseError:一把抓住本模块的所有业务异常。
class AppError(Exception): ...
class ConfigError(AppError): ...
class DatabaseError(AppError): ...
def load_config(path):
if not path.endswith(".yaml"):
raise ConfigError("仅支持 .yaml 配置文件")
return {"ok": True}
# === 调用演示 ===
try:
load_config("conf.json")
except AppError as e: # 统一入口
print("应用错误:", type(e).__name__, "-", e)
# 预期输出:
# 应用错误: ConfigError - 仅支持 .yaml 配置文件
6. 资源清理:finally 与 with(上下文管理)¶
- 在
finally中无条件清理资源(关闭文件/连接/锁)。 - 推荐
with ... as ...:由对象实现__enter__ / __exit__自动管理。
# finally 方式
def read_first_line(path):
f = open(path, "w+", encoding="utf-8")
try:
f.write("hello\nworld")
f.seek(0)
return f.readline().strip()
finally:
f.close()
# with 方式(更推荐)
def write_hello(path):
with open(path, "w", encoding="utf-8") as f:
f.write("Hello")
print(read_first_line("tmp.txt")) # -> hello
write_hello("tmp2.txt")
print(open("tmp2.txt", "r", encoding="utf-8").read()) # -> Hello
自定义上下文管理器
- 实现对象的
__enter__/__exit__;或用contextlib.contextmanager装饰器写生成器式管理器。
from contextlib import contextmanager
@contextmanager
def opened(path, mode="w", encoding="utf-8"):
f = open(path, mode, encoding=encoding)
try:
yield f
finally:
f.close()
with opened("demo.txt") as f:
f.write("OK")
print(open("demo.txt","r",encoding="utf-8").read()) # -> OK
7. 断言(assert):开发期自检,而非业务分支¶
- 语义:在开发/测试阶段校验“永远应该为真”的条件;失败抛
AssertionError。 - 生产环境可被优化关闭(
python -O),因此不要用assert做业务逻辑。
def normalize(pct):
assert 0.0 <= pct <= 1.0, "pct 应在 [0,1]"
return f"{pct:.1%}"
print(normalize(0.256)) # -> 25.6%
# normalize(1.5) # 取消注释会触发 AssertionError
8. 日志与重试:记录现场,合理兜底¶
- 用
logging记录异常类型、消息、栈;必要时重试(注意退避/上限)。 - 重试适合临时性错误(网络抖动、瞬时超时),不适合参数错误。
import logging, time, random
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
def flaky_call():
if random.random() < 0.6:
raise TimeoutError("超时")
return "OK"
def call_with_retry(max_retries=3, delay=0.1):
for i in range(1, max_retries + 1):
try:
return flaky_call()
except TimeoutError as e:
logging.warning("第 %d 次失败:%s", i, e)
time.sleep(delay * i) # 线性退避
raise RuntimeError("重试仍失败")
# === 调用演示(可能因随机性失败/成功) ===
try:
print(call_with_retry())
except Exception as e:
print("最终失败:", e)
9. 常见反模式与修正¶
- 裸
except:→ 换成精确异常或except Exception as e:并记录日志后抛出/返回明确错误。 - 吞异常(只打印不处理)→ 日志 + 重新抛出或返回可判定的错误值。
- 过度广捕(捕太多类型)→ 针对不同错误分支,精细化处理。
- 用异常做正常流程(如循环结束靠异常)→ 用清晰的条件/返回值控制。
# 反例:吞异常
try:
1/0
except Exception:
pass # 错误被静默,问题难以排查
# 修正:
import logging
try:
1/0
except ZeroDivisionError as e:
logging.exception("计算失败") # 带栈日志
raise # 继续向上抛,让调用方知晓
10. 实战范式清单(可直接套用)¶
- 函数边界:先做输入校验(类型/取值范围),不满足就
raise ValueError/TypeError。 - 资源操作(文件/网络/数据库):用
with包裹;在外层捕获并加上业务语义后再抛。 - 服务调用:按重试策略区分“可重试 vs 不可重试”错误。
- 库/框架交互:阅读文档了解它会抛什么异常,针对性处理。
- 对外接口:统一捕获你的自定义业务异常,转为 HTTP/消息队列的错误码与清晰文案。
11. 练习¶
- 写
safe_read(path):读文件的第一行,文件不存在返回"MISSING",其他异常记录日志后抛出。 - 写
retry(func, retries=3, backoff=0.2):可配置重试的装饰器,只对TimeoutError生效。 - 设计异常层级:
PaymentError(基类)→CardError、BalanceError,并在支付流程中使用。