第 8 章 异常处理与调试

8.1 异常是什么

程序运行中的错误叫异常(Exception)。未捕获的异常会终止程序并打印回溯(traceback):

1 / 0
# ZeroDivisionError: division by zero

学会读 traceback:从下往上看——最后一行是异常类型和消息,往上每一层 File "..." 是调用经过的文件与行号,最先定位到最后一行自己代码的行号。

常见内置异常:

异常触发场景
NameError使用未定义的变量
TypeError类型不匹配的操作("1" + 1)
ValueError类型对但值非法(int("abc"))
IndexError / KeyError索引越界 / 字典键不存在
FileNotFoundError文件不存在
ZeroDivisionError除零
AttributeError对象没有该属性/方法
ImportError / ModuleNotFoundError导入失败

完整层级可用 help("exceptions") 查看;所有异常都继承自 BaseException(业务异常继承 Exception)。

8.2 try / except:捕获与处理

try:
    age = int(input("年龄:"))
except ValueError:
    print("请输入数字!")

工作方式:执行 try 块,一旦抛出异常,立即跳到第一个匹配的 except 块。

捕获多个异常

try:
    risky()
except (ValueError, TypeError) as e:   # 元组打包 + as 拿到异常对象
    print(f"出错:{e}")
except Exception as e:                 # 兜底(放最后)
    print(f"意外错误:{e}")

不要裸 except:(会连 Ctrl-C 的 KeyboardInterrupt 一起吞掉),最低限度写 except Exception。

不要吞异常——捕获后什么都不做(空的 except 块)会让 bug 无声消失:

try:
    process()
except Exception:
    pass              # 坏味道!至少要 log 或 raise

else 与 finally

try:
    f = open("data.txt")
except FileNotFoundError:
    print("文件不存在")
else:
    # 仅当 try 没抛异常时执行 —— 放"成功路径"
    print(f.read())
finally:
    # 无论如何都执行(清理资源)—— 通常用 with 替代
    print("done")

执行顺序:try 正常 → else → finally;try 抛异常 → 匹配的 except → finally。finally 一定会执行(即使有 return/未捕获异常)。

8.3 主动抛出:raise

def set_age(age):
    if not isinstance(age, int):
        raise TypeError("age 必须是整数")
    if age < 0:
        raise ValueError(f"age 不能为负数,收到 {age}")

在 except 中重新抛出(记录后不吞):

try:
    connect()
except TimeoutError:
    log("连接超时,将重试")
    raise                    # 裸 raise 原样再抛,保留原 traceback

raise NewError(...) from e 表示"由 e 引起",保留因果链。

8.4 自定义异常

业务项目里定义自己的异常层级:

class AppError(Exception):
    """应用基础异常"""

class ValidationError(AppError):
    pass

class PaymentError(AppError):
    pass

# 调用方可统一捕获 AppError,也可细分处理

命名以 Error 结尾,构造消息写清楚"出了什么问题、关键值是什么"。

8.5 断言 assert

def discount(price):
    assert price > 0, "price 必须为正"   # 不满足抛 AssertionError

注意:python -O 运行时 assert 全部被忽略,因此绝不能用 assert 做输入校验或业务判断——它只用于开发期自检(不变式)。运行期校验用 raise。

8.6 上下文管理器 with:资源管理的正解

文件、锁、网络连接等资源用 with 管理,离开块时自动清理(即使中途抛异常):

with open("data.txt", encoding="utf-8") as f:
    content = f.read()
# 离开 with 块文件已自动关闭,无需 f.close()

原理是魔术方法 __enter__ / __exit__(见第 6 章)。自定义可复用:

from contextlib import contextmanager

@contextmanager
def timer(name):
    import time
    start = time.perf_counter()
    try:
        yield                      # with 块体在这里执行
    finally:
        print(f"{name} 耗时 {time.perf_counter() - start:.3f}s")

with timer("处理"):
    sum(range(10_000_000))

8.7 调试手段

① print 大法(快速但原始)

print(f"{x=}, {type(x)=}")    # f-string 调试语法(3.8+)

② logging(正规军)

import logging
logging.basicConfig(level=logging.DEBUG,
                    format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)

log.debug("变量 x = %s", x)    # 惰性格式化
log.info("开始处理")
log.warning("接近阈值")
log.error("失败", exc_info=True)   # 附带 traceback

分级:DEBUG < INFO < WARNING < ERROR < CRITICAL。设 level 控制输出粒度。

③ 断点调试器 pdb

# 代码中插入断点(3.7+ 推荐写法):
breakpoint()

进入 pdb 后常用命令:n(下一行)s(步入)c(继续)p x(打印变量)q(退出)。VS Code / PyCharm 的图形断点本质相同,日常开发更推荐 IDE 断点。

④ 静态检查

pip install ruff mypy
ruff check .      # 代码质量与风格
mypy .            # 类型检查(配合类型提示)

8.8 EAFP 与 LBYL

Python 社区两种风格:

# LBYL:三思而后行(Look Before You Leap)
if "key" in d:
    v = d["key"]

# EAFP:请求原谅比请求许可容易(Easier to Ask Forgiveness than Permission)
try:
    v = d["key"]
except KeyError:
    v = default

Python 偏爱 EAFP——异常开销在"不发生"时几乎为零,且没有竞态问题。但两者都对:频繁预期的失败(如解析用户输入)用 try;简单判断用 in/get。

8.9 本章小结

  • traceback 从下往上读;先认全常见异常类型;

  • 捕获要精确,永远别静默吞异常;finally 必执行,资源用 with;

  • raise 主动抛错、裸 raise 重抛;业务异常自建层级;

  • assert 只做开发期自检;生产校验用 raise;

  • 调试三件套:print(f"{x=}") → logging → 断点。

下一章:文件与数据格式。

Python3 基础知识 / 第 8 章 异常处理与调试 0 字 0 行 cosolar
2026-09-17T11:10:47.745034023Z 2026-09-17T11:25:54.855610180Z