🎯 学习目标
- 区分语法错误与运行时异常,能读懂 Traceback 调用栈
- 掌握 Python 常见异常类型及继承体系
- 熟练使用
try/except/else/finally处理异常 - 会用
raise主动抛异常、会自定义异常类 - 理解 EAFP 风格与异常处理最佳实践
- 掌握 print、断言、logging、pdb、IDE 断点等调试手段
1两类错误
语法错误(SyntaxError):根本跑不起来
if True # SyntaxError: expected ':' 少了冒号
print("hi")
print("未闭合) # 括号/引号未闭合、缩进错误都属于此类
语法错误在解释器解析阶段就会被发现,程序一行都不会执行。VS Code 通常在编写时就用红线标出。
运行时异常(Exception):执行到某处才崩
nums = [1, 2]
print(nums[5]) # IndexError: list index out of range
print(10 / 0) # ZeroDivisionError: division by zero
int("abc") # ValueError: invalid literal
print(undefined) # NameError: name 'undefined' is not defined
读懂 Traceback(报错栈)
Traceback (most recent call last):
File "app.py", line 10, in <module> ← 最外层
main()
File "app.py", line 7, in main ← 调用链向下追
process(data)
File "app.py", line 3, in process ← 出错的最内层(重点看这里!)
return int(data)
ValueError: invalid literal for int() with base 10: 'abc'
↑异常类型 ↑具体错误原因
读报错的顺序:从最后一行看异常类型和原因,从倒数第一个 File 行定位出错文件与行号,再向上看调用链。不要被一大片红字吓到。
2常见异常与继承体系
BaseException
└── Exception # 所有"常规异常"的基类
├── ValueError # 值的类型对但内容非法:int("abc")
├── TypeError # 类型不对:"a" + 1
├── KeyError # 字典键不存在
├── IndexError # 序列下标越界
├── AttributeError # 对象没有该属性/方法
├── NameError # 变量名未定义
├── ZeroDivisionError # 除数为 0
├── FileNotFoundError # 文件不存在
├── ImportError/ModuleNotFoundError # 导入失败
├── StopIteration # 迭代结束
└── ...
| 异常 | 触发场景 | 例子 |
|---|---|---|
ValueError | 值无法转换/不合法 | int("3.5") |
TypeError | 类型不支持该操作 | len(123) |
KeyError | dict 取不存在的键 | {}["a"] |
IndexError | list/tuple 越界 | [1][2] |
AttributeError | 属性/方法不存在 | None.strip() |
FileNotFoundError | 打开不存在的文件 | open("x.txt") |
UnicodeDecodeError | 解码编码不匹配 | GBK 字节按 UTF-8 解 |
3try-except:优雅地处理异常
# 基础结构
try:
num = int(input("输入一个整数:"))
print(10 / num)
except ValueError:
print("输入的不是合法整数")
except ZeroDivisionError:
print("0 不能作除数")
except Exception:
print("发生了其他错误")
else:
print("没有异常时才执行")
finally:
print("无论是否异常都会执行(常用于关闭资源)")
各代码块职责
| 块 | 执行时机 | 典型用途 |
|---|---|---|
try | 放可能出错的代码 | 只包"可能失败"的最小片段 |
except 类型 | 发生匹配异常时 | 针对性处理/降级/提示 |
else | 没发生异常时 | 成功后的后续逻辑(保持 try 精简) |
finally | 无论如何都执行(即使 return/崩之前) | 关闭文件、释放连接 |
一次捕获多种异常
try:
...
except (ValueError, TypeError) as e: # 元组合并捕获
print("数据有问题:", e) # as e 拿到异常对象,e 就是错误信息
print(type(e).__name__) # 'ValueError'
# 捕获顺序:先具体后宽泛!
try:
...
except FileNotFoundError: # 子类异常写前面
print("文件不存在")
except OSError: # 父类兜底写后面
print("其他 IO 错误")
except Exception: # 最宽泛的常规异常
print("未知错误")
不要写裸 except:(连 Ctrl+C 的 KeyboardInterrupt 都能吞掉)或 except Exception: pass——它会隐藏所有错误,让 bug 极难排查。至少要记录日志或只捕获你预期的具体类型。
finally 与 return 的执行顺序
def f():
try:
return 1
finally:
print("finally 仍会执行") # return 前先打印这句,再真正返回
print(f())
4raise:主动抛出异常
def set_age(age):
if not isinstance(age, int):
raise TypeError("年龄必须是整数")
if age < 0 or age > 150:
raise ValueError(f"年龄不合法:{age}")
return age
try:
set_age(-1)
except ValueError as e:
print("捕获:", e) # 捕获: 年龄不合法:-1
也可以把捕获到的异常重新抛出(记录日志后向上传播):
try:
int("abc")
except ValueError:
print("记录日志:转换失败")
raise # 不带参数:原样继续向上抛
5自定义异常
业务错误用自定义异常表达,语义清晰、方便上层分别处理:
# 约定:继承 Exception(而不是 BaseException)
class NLPError(Exception):
"""NLP 模块基础异常。"""
class EmptyTextError(NLPError):
"""输入文本为空。"""
class UnsupportedLangError(NLPError):
"""不支持的语言。"""
def tokenize(text, lang="zh"):
if not text or not text.strip():
raise EmptyTextError("待分词文本不能为空")
if lang not in ("zh", "en"):
raise UnsupportedLangError(f"暂不支持语言:{lang}")
return list(text)
try:
tokenize(" ")
except NLPError as e: # 一次接住本模块所有自定义异常
print("处理失败:", type(e).__name__, e)
6异常处理最佳实践
LBYL vs EAFP
LBYL(先看再跳,C 风格)
d = {}
if "key" in d and isinstance(d["key"], int):
print(d["key"] + 1)
else:
print("数据不可用")
EAFP(先做再说,Python 风格)
d = {}
try:
print(d["key"] + 1)
except (KeyError, TypeError):
print("数据不可用")
Python 官方推荐 EAFP(Easier to Ask Forgiveness than Permission):异常开销低,代码更关注主流程。但用户输入、文件、网络等"正常也会失败"的场景,优先校验给出友好提示。
实战原则:① 只捕获你能处理的异常;② except 范围尽量小、具体;③ 在边界层(用户输入、读文件、请求接口)统一处理并给出友好提示,内部代码让异常快速暴露;④ 用 logging.exception() 记录完整堆栈。
7程序调试五件套
① print 大法
# f-string 的 = 说明符是调试利器(3.8+)
def buggy(a, b):
print(f"{a=}, {b=}") # a=1, b=0
result = a / b
print(f"{result=}")
return result
# buggy(1, 0)
② assert 断言
def avg(nums):
assert len(nums) > 0, "nums 不能为空" # 条件不成立立即 AssertionError
return sum(nums) / len(nums)
# 断言用于开发期捕捉"绝不该发生"的情况;不要用它做用户输入校验
# (python -O 优化模式会跳过断言)
③ logging 日志(项目级首选)
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
filename="app.log", # 不加则输出到控制台
encoding="utf-8"
)
logging.debug("细节信息") # 默认不显示
logging.info("普通流程信息")
logging.warning("警告")
logging.error("出错了")
try:
1 / 0
except ZeroDivisionError:
logging.exception("除零现场") # 自动附带完整堆栈,排查神器
④ pdb 交互式调试(标准库自带)
import pdb
def f(n):
total = 0
for i in range(n):
total += i
pdb.set_trace() # 程序在此暂停,进入交互式调试
return total
# f(3)
# 常用命令:n(下一行) s(进入函数) c(继续) p 变量名(查看)
# l(看代码) b 行号(断点) q(退出) w(调用栈)
# Python 3.7+ 也可直接用命令行:python -m pdb app.py
# VS Code 中在代码里写 breakpoint() 即可触发可视化调试
⑤ IDE 断点调试(日常效率最高)
- 在 VS Code 行号左侧点一下,出现红点 = 断点。
- 按
F5(或菜单 运行→启动调试)选择 Python File。 - 程序停在断点后,可:悬停查看变量、在左侧面板监视表达式、F10 单步跳过 / F11 单步进入、在调试控制台直接输入表达式求值。
8综合案例:健壮的文件读取函数
import logging
def read_text_file(path, encoding="utf-8"):
"""安全读取文本文件,失败时返回 None 并记录日志。"""
try:
with open(path, "r", encoding=encoding) as f:
return f.read()
except FileNotFoundError:
logging.warning("文件不存在:%s", path)
except UnicodeDecodeError:
logging.error("编码错误,请确认文件是否为 %s 编码", encoding)
except OSError as e:
logging.exception("读取文件发生 IO 错误:%s", e)
return None
content = read_text_file("maybe_exist.txt")
if content is not None:
print("读取成功,长度", len(content))
else:
print("使用默认内容继续运行…")
9本章小结与练习
📌 关键结论
- 看 Traceback:最后一行看类型原因,最内层 File 行看出错位置。
- except 先具体后宽泛;禁止裸
except: pass吞错误。 finally一定执行,适合释放资源;raise主动报错,自定义异常继承 Exception。- 边界层统一处理异常;内部代码宁可让问题早暴露。
- 调试组合:print(
f"{x=}") → 断点调试 → logging 长期保留。
练习1:d = {}; print(d["a"]) 会抛什么异常?给出两种不让程序崩溃的写法。
KeyError。写法一:print(d.get("a")),键不存在返回 None 不报错;写法二:try: print(d["a"]) except KeyError: print("无此键")。前者适合取默认值,后者适合更复杂的失败处理。
练习2:except 的排列顺序有什么要求?为什么不推荐裸 except?
多个 except 要先写具体子类异常(FileNotFoundError),后写宽泛父类(OSError、Exception),否则子类永远匹配不到。裸 except 会捕获包括 KeyboardInterrupt、SystemExit 在内的一切异常并静默吞掉,导致程序无法正常中断、bug 被隐藏,应至少捕获 Exception 并记录处理。
练习3:写一个 safe_int(s) 函数,能转则转,不能转返回 None(不抛异常)。
def safe_int(s):
try:
return int(s)
except (ValueError, TypeError):
return None 注意还要捕获 TypeError(s 为 None 等非字符串时 int(None) 抛 TypeError)。