01-错误与异常基础
Python 版本要求: 本教程基于 Python 3.11+ 编写,部分特性需要 3.11 或更高版本。
导航: 错误与异常基础 → 异常处理 → 抛出异常 → 上下文管理器
概念铺垫
第一步:实际场景引入
为什么需要了解错误与异常?
假设你正在编写一个计算器程序:
python
def calculate(expression: str) -> float:
"""计算数学表达式"""
return eval(expression)
# 用户输入
result = calculate(input("请输入表达式:"))问题来了:
用户输入: 10 / 0
程序报错: ZeroDivisionError: division by zero
程序崩溃!💥
用户输入: abc
程序报错: SyntaxError
程序崩溃!💥
用户输入: (缺少括号
程序报错: SyntaxError: unexpected EOF
程序崩溃!💥思考: 程序总是意外崩溃,用户体验极差。如何让程序优雅地处理这些错误?
第二步:概念动机
错误与异常的关系
┌─────────────────────────────────────────┐
│ 错误与异常的关系 │
├─────────────────────────────────────────┤
│ │
│ 错误(Error) │
│ ───────────────── │
│ 程序执行过程中出现的问题 │
│ │
│ 异常(Exception) │
│ ───────────────── │
│ 错误的一种,程序可以捕获和处理 │
│ │
│ ───────────────────────────────── │
│ │
│ 错误的分类: │
│ │
│ 错误 │
│ ├── 语法错误(SyntaxError) │
│ │ 代码写错了,无法运行 │
│ │ │
│ └── 运行时错误(Exception) │
│ 代码语法正确,但运行时出错 │
│ 可以被捕获和处理 │
│ │
└─────────────────────────────────────────┘核心理解:
- 语法错误:代码写错,解释器无法理解,必须修正才能运行
- 异常:代码语法正确,但运行时遇到问题,可以被捕获处理
L1 理解层:会用
第三步:最简示例
语法错误 vs 运行时异常
python
# 语法错误:代码还没运行就报错
# if True # ❌ SyntaxError: expected ':'
# 运行时异常:代码能运行,但执行时出错
result = 10 / 0 # ❌ ZeroDivisionError区别:
| 类型 | 发现时机 | 能否捕获 | 处理方式 |
|---|---|---|---|
| 语法错误 | 运行前 | 不能 | 修正代码 |
| 异常 | 运行时 | 能 | try-except |
第四步:详细讲解
语法错误(SyntaxError)
语法错误在代码运行前就能发现,Python 解释器无法解析代码。
python
# ─────────────────────────────────────
# 示例 1:缺少冒号
# ─────────────────────────────────────
# if True
# print("Hello")
# ❌ SyntaxError: invalid syntax
# 正确写法:
if True:
print("Hello")
# ─────────────────────────────────────
# 示例 2:缺少括号
# ─────────────────────────────────────
# print("Hello"
# ❌ SyntaxError: unexpected EOF while parsing
# 正确写法:
print("Hello")
# ─────────────────────────────────────
# 示例 3:缩进错误
# ─────────────────────────────────────
# def greet():
# print("Hello")
# ❌ IndentationError: expected an indented block
# 正确写法:
def greet() -> None:
print("Hello")语法错误的特点:
- 在代码运行前就能发现
- Python 解释器无法解析代码
- 无法通过 try-except 捕获
- 必须修正代码才能运行
常见异常类型
异常类型速查
┌─────────────────────────────────────────┐
│ 常见异常类型速查 │
├─────────────────────────────────────────┤
│ │
│ NameError │
│ 使用了未定义的变量 │
│ print(undefined_var) │
│ │
│ TypeError │
│ 类型错误,操作应用于不适当的类型 │
│ "5" + 3 # 字符串不能和数字相加 │
│ │
│ ValueError │
│ 值错误,类型正确但值不合适 │
│ int("abc") # "abc"不能转为整数 │
│ │
│ IndexError │
│ 索引越界 │
│ [1, 2, 3][10] # 索引超出范围 │
│ │
│ KeyError │
│ 字典键不存在 │
│ {"a": 1}["b"] # 键 "b"不存在 │
│ │
│ AttributeError │
│ 属性或方法不存在 │
│ "str".not_exist() # 方法不存在 │
│ │
│ FileNotFoundError │
│ 文件不存在 │
│ open("not_exist.txt") # 文件不存在 │
│ │
│ ZeroDivisionError │
│ 除以零 │
│ 10 / 0 # 除零错误 │
│ │
└─────────────────────────────────────────┘异常示例详解
python
# ─────────────────────────────────────
# NameError: 变量未定义
# ─────────────────────────────────────
# print(name) # name 未定义
# ❌ NameError: name 'name' is not defined
name: str = "Alice"
print(name) # ✅ Alice
# ─────────────────────────────────────
# TypeError: 类型错误
# ─────────────────────────────────────
# result = "5" + 3
# ❌ TypeError: can only concatenate str (not "int") to str
# 正确写法:
result1: str = "5" + str(3) # "53"
result2: int = int("5") + 3 # 8
# ─────────────────────────────────────
# ValueError: 值错误
# ─────────────────────────────────────
# age = int("abc")
# ❌ ValueError: invalid literal for int() with base 10: 'abc'
# 正确写法:
age: int = int("123") # 123
# ─────────────────────────────────────
# IndexError: 索引越界
# ─────────────────────────────────────
numbers: list[int] = [1, 2, 3]
# value = numbers[10]
# ❌ IndexError: list index out of range
# 安全访问:
if len(numbers) > 10:
value: int = numbers[10]
# ─────────────────────────────────────
# KeyError: 键不存在
# ─────────────────────────────────────
person: dict[str, int | str] = {"name": "Alice", "age": 25}
# city = person["city"]
# ❌ KeyError: 'city'
# 安全访问:
city: str = person.get("city", "未知") # "未知"
# ─────────────────────────────────────
# ZeroDivisionError: 除零错误
# ─────────────────────────────────────
# result = 10 / 0
# ❌ ZeroDivisionError: division by zero
# 安全计算:
divisor: int = 0
if divisor != 0:
result: float = 10 / divisor
else:
result: float = float('inf') # 无穷大第五步:渐进复杂化
异常的层次结构
┌─────────────────────────────────────────┐
│ 异常继承层次 │
├─────────────────────────────────────────┤
│ │
│ BaseException │
│ └── Exception │
│ ├── ValueError │
│ ├── TypeError │
│ ├── IndexError │
│ ├── KeyError │
│ ├── FileNotFoundError │
│ └── ... │
│ │
│ 理解层次的好处: │
│ • 可以捕获一类异常 │
│ • 可以精确捕获特定异常 │
│ │
└─────────────────────────────────────────┘判断异常类型
python
def safe_divide(a: float, b: float) -> float | None:
"""安全除法,返回 None 表示错误"""
try:
return a / b
except ZeroDivisionError:
print("除数不能为零")
return None
except TypeError:
print("参数类型错误")
return None
# 使用
result1: float | None = safe_divide(10, 2) # 5.0
result2: float | None = safe_divide(10, 0) # None,打印错误
result3: float | None = safe_divide("10", 2) # None,打印错误第六步:实际应用
用户输入验证
python
def get_integer_input(prompt: str) -> int | None:
"""
获取用户输入的整数。
Args:
prompt: 提示信息
Returns:
用户输入的整数,如果输入无效返回 None
"""
try:
user_input: str = input(prompt)
return int(user_input)
except ValueError:
print("❌ 输入无效,请输入一个整数")
return None
def main() -> None:
"""主程序"""
print("=== 整数计算器 ===")
while True:
num1: int | None = get_integer_input("请输入第一个数(或 q 退出):")
if num1 is None:
continue
num2: int | None = get_integer_input("请输入第二个数:")
if num2 is None:
continue
try:
result: int = num1 + num2
print(f"结果:{num1} + {num2} = {result}")
except Exception as e:
print(f"计算错误:{e}")
if __name__ == "__main__":
main()文件读取安全处理
python
from pathlib import Path
def read_config(filepath: str) -> dict[str, str] | None:
"""
安全读取配置文件。
Args:
filepath: 配置文件路径
Returns:
配置字典,如果文件不存在或格式错误返回 None
"""
path: Path = Path(filepath)
if not path.exists():
print(f"❌ 配置文件不存在:{filepath}")
return None
try:
content: str = path.read_text(encoding="utf-8")
config: dict[str, str] = {}
for line in content.strip().split("\n"):
if "=" in line and not line.startswith("#"):
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
return config
except Exception as e:
print(f"❌ 读取配置文件失败:{e}")
return None
# 使用
config: dict[str, str] | None = read_config("config.ini")
if config:
print(f"配置加载成功:{config}")关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
-> int | None | 返回值为整数或 None | 输入无效时返回 None 表示"无结果",比抛异常更适合交互式输入场景 |
except ValueError: return None | 捕获类型转换失败 | 只捕获已知的具体异常,不用宽泛的 except Exception 掩盖真正的 bug |
if not path.exists(): return None | 在 try 外提前检查文件存在性 | 文件不存在是可预期情况,提前返回比捕获异常更清晰 |
line.split("=", 1) | 只切分一次 | 配置值中可能含 =(如 URL),maxsplit=1 保证只拆成 key/value 两部分 |
except Exception as e: return None | 兜底捕获所有异常 | 文件读取可能遇到编码错误、权限错误等,统一返回 None 而不让异常透传 |
L2 实践层:最佳实践
异常类型选择指南
如何选择合适的异常类型
┌─────────────────────────────────────────────────────────────────┐
│ 异常类型选择决策树 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 问题类型? │
│ │ │
│ ├─→ 参数类型错误 ──→ TypeError │
│ │ 例:函数期望 int,实际传入 str │
│ │ │
│ ├─→ 参数值无效 ──→ ValueError │
│ │ 例:int("abc")、负数年龄 │
│ │ │
│ ├─→ 索引/键不存在 ──→ IndexError / KeyError │
│ │ 例:list[100]、dict["missing"] │
│ │ │
│ ├─→ 资源不存在 ──→ FileNotFoundError / FileNotFoundError │
│ │ 例:文件、目录、网络资源 │
│ │ │
│ ├─→ 权限问题 ──→ PermissionError │
│ │ 例:无写入权限、无执行权限 │
│ │ │
│ ├─→ 操作不支持 ──→ NotImplementedError │
│ │ 例:子类未实现抽象方法 │
│ │ │
│ └─→ 业务规则违反 ──→ 自定义异常 │
│ 例:余额不足、库存不够 │
│ │
└─────────────────────────────────────────────────────────────────┘推荐做法
| 场景 | 推荐异常 | 原因 | 示例 |
|---|---|---|---|
| 类型不匹配 | TypeError | 明确指出类型问题 | "expected int, got str" |
| 值超出范围 | ValueError | 类型正确但值不合法 | int("abc")、年龄为负 |
| 列表索引越界 | IndexError | 明确是索引问题 | list[100] |
| 字典键缺失 | KeyError | 明确是键查找问题 | dict["missing"] |
| 文件不存在 | FileNotFoundError | 明确是文件系统问题 | open("missing.txt") |
| 功能未实现 | NotImplementedError | 明确是开发阶段问题 | 抽象方法未实现 |
| 业务规则违反 | 自定义异常 | 业务语义清晰 | InsufficientBalanceError |
异常类型的语义对比
┌─────────────────────────────────────────────────────────────────┐
│ TypeError vs ValueError vs 自定义异常 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ TypeError │
│ ───────────────────────────────────────────────────────── │
│ 含义:你给的类型和我期望的不一样 │
│ 场景:函数签名要求 int,你传了 str │
│ 例:len(123) # int 没有 __len__ │
│ "5" + 3 # str 不能和 int 相加 │
│ │
│ ValueError │
│ ───────────────────────────────────────────────────────── │
│ 含义:类型是对的,但值不合法 │
│ 场景:int 类型正确,但 "abc"无法转换为整数 │
│ 例:int("abc") # str 类型正确,值无法解析 │
│ datetime(2024, 2, 30) # 2月没有30号 │
│ │
│ 自定义异常 │
│ ───────────────────────────────────────────────────────── │
│ 含义:业务层面的错误条件 │
│ 场景:余额不足、库存不够、权限级别不够 │
│ 例:raise InsufficientBalanceError("余额不足") │
│ raise OutOfStockError("商品售罄") │
│ │
└─────────────────────────────────────────────────────────────────┘常见异常陷阱
反模式:不要这样做
python
# ❌ 陷阱 1:捕获太宽泛的异常
try:
result = some_operation()
except Exception: # 捕获所有异常,包括系统异常
print("出错了")
# 问题:
# 1. 捕获了不应该捕获的异常(如 KeyboardInterrupt)
# 2. 隐藏了真正的错误原因
# 3. 调试困难,无法定位问题python
# ✅ 正确做法:捕获具体的异常
try:
result = some_operation()
except ValueError as e:
print(f"值错误:{e}")
except TypeError as e:
print(f"类型错误:{e}")python
# ❌ 陷阱 2:捕获后不做任何处理
try:
data = json.loads(json_str)
except json.JSONDecodeError:
pass # 吞掉异常,继续执行
# 问题:
# 1. 错误被完全忽略
# 2. 后续代码可能使用无效数据
# 3. 问题会延迟暴露,更难排查python
# ✅ 正确做法:至少记录日志
import logging
try:
data = json.loads(json_str)
except json.JSONDecodeError as e:
logging.error(f"JSON 解析失败:{e}")
data = {} # 使用默认值python
# ❌ 陷阱 3:用异常控制正常流程
def find_item(items: list, target: str) -> int:
try:
return items.index(target)
except ValueError:
return -1
# 问题:
# 1. 异常比条件判断慢 10-100 倍
# 2. 用异常做"正常分支"违反语义
# 3. 异常本意是"异常情况",不是"控制流"python
# ✅ 正确做法:先检查再操作
def find_item(items: list, target: str) -> int:
if target in items:
return items.index(target)
return -1
# 或使用 dict.get() 模式
result = my_dict.get("key", default_value) # 不抛异常python
# ❌ 陷阱 4:在 finally 中返回值
def bad_function() -> int:
try:
return 1
finally:
return 2 # 会覆盖 try 的返回值!
# 问题:
# finally 总是执行,return 2 会覆盖 return 1
# 调用者永远得到 2,try 块的返回值被吞掉python
# ✅ 正确做法:finally 只做清理
def good_function() -> int:
resource = None
try:
resource = acquire_resource()
return process(resource)
finally:
if resource:
release_resource(resource) # 只清理,不返回常见陷阱汇总表
| 陷阱 | 错误做法 | 问题 | 正确做法 |
|---|---|---|---|
| 捕获太宽泛 | except Exception: | 隐藏真正错误,吞掉系统异常 | 捕获具体异常类型 |
| 空异常处理 | except: pass | 错误完全被忽略,延迟暴露 | 至少记录日志或设置默认值 |
| 异常做控制流 | 用异常做正常分支判断 | 性能差,语义错误 | 用条件判断或 .get() |
| finally 返回值 | finally: return x | 覆盖 try 的返回值 | finally 只做清理,不返回 |
| 捕获顺序错误 | 先 Exception 后具体 | 具体异常永远不执行 | 从具体到宽泛的顺序 |
| 资源未释放 | 异常发生但资源未关闭 | 资源泄漏(文件、连接) | 用 with 语句或 finally |
适用场景
异常处理的适用场景
| 场景 | 是否推荐用异常 | 原因 | 替代方案 |
|---|---|---|---|
| 用户输入验证 | ✅ 推荐 | 输入错误是真正的异常情况 | - |
| 文件/网络操作 | ✅ 推荐 | 外部资源不可控,异常是常态 | 先检查 exists() 再操作 |
| 数据库操作 | ✅ 推荐 | 连接、事务失败需要处理 | - |
| API 调用 | ✅ 推荐 | 网络错误、认证失败需处理 | - |
| 配置加载 | ✅ 推荐 | 配置缺失/错误影响启动 | 提供默认配置 |
| 查找元素(不存在) | ❌ 不推荐 | 不存在是常见的正常情况 | 用 in 检查或 .get() |
| 循环遍历 | ❌ 不推荐 | 遍历结束是正常行为 | 用 for 循环自动处理 |
| 参数默认值 | ❌ 不推荐 | 默认值是设计的一部分 | 直接设置默认值 |
查找操作的选择指南
python
# 场景:查找元素,可能不存在
# ❌ 用异常处理"不存在"(性能差)
try:
value = my_dict["key"]
except KeyError:
value = None
# ✅ 用 get() 方法(推荐)
value = my_dict.get("key") # 不存在返回 None
value = my_dict.get("key", "default") # 不存在返回默认值
# ✅ 用 in 检查(需要额外逻辑时)
if "key" in my_dict:
value = my_dict["key"]
process(value)
else:
handle_missing()L3 专家层:深入
Python 如何实现错误检测
Python 在运行时会经历四个阶段:源代码读取 → 解析(语法检查)→ 编译(字节码生成)→ 执行(PVM 运行)。语法错误在前两个阶段就被拦截,异常则发生在执行阶段。
Python 错误检测流程:
┌─────────────────────────────────────────────────────────────┐
│ Python 错误检测的时机 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 源码 .py │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 解析器(Parser) │ │
│ │ 词法分析 → 语法分析 → AST 生成 │ │
│ │ │ │
│ │ 此阶段捕获: │ │
│ │ ・ SyntaxError(语法结构错误) │ │
│ │ ・ IndentationError(缩进错误) │ │
│ │ │ │
│ │ 无法通过 try-except 捕获! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ (解析通过) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 编译器(Compiler) │ │
│ │ AST → 字节码对象 (__code__) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ (编译通过) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Python 虚拟机(PVM) │ │
│ │ 执行字节码 │ │
│ │ │ │
│ │ 此阶段发生: │ │
│ │ ・ 所有 Exception 子类(ZeroDivisionError 等) │ │
│ │ ・ 可以通过 try-except 捕获 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘验证代码:
python
import ast
import sys
# 演示语法错误在编译前被捕获
try:
compile("if True", "<test>", "exec")
except SyntaxError as e:
print(f"语法错误: {e}")
print(f"发生于: 文件={e.filename}, 行={e.lineno}, 列={e.offset}")
# 演示运行时异常触发的位置
def trace_exception():
try:
10 / 0
except ZeroDivisionError as e:
print(f"异常类型: {type(e).__name__}")
print(f"异常所在帧: {sys._getframe()}")异常层次结构的实现
Python 使用 C 扩展类型系统实现异常类的继承关系:
异常对象创建流程:
┌─────────────────────────────────────────────────────────────┐
│ BaseException 的底层实现 │
├─────────────────────────────────────────────────────────────┤
│ │
│ BaseException.__new__() │
│ │ │
│ ├─ 设置 __traceback__ 属性 │
│ ├─ 设置 __cause__ (异常链中的原因) │
│ ├─ 设置 __context__ (异常处理时的新异常) │
│ ├─ 设置 args 元组 │
│ │ │
│ └─ 被捕获时: │
│ 1. 解释器暂停当前帧 │
│ 2. 沿调用栈向上查找匹配的 except 块 │
│ 3. 找到:将异常对象绑定到 as 变量 │
│ 4. 未找到:打印 traceback 并终止程序 │
│ │
│ Exception vs BaseException: │
│ ─────────────────────────── │
│ BaseException 子类: │
│ KeyboardInterrupt, SystemExit, GeneratorExit │
│ 这些通常不应被捕获(裸 except: 除外,但不建议) │
│ │
│ Exception 子类: │
│ 所有"常规"异常(ValueError, TypeError...) │
│ 应该只捕获 Exception 及其子类 │
│ │
└─────────────────────────────────────────────────────────────┘验证代码:
python
# 查看异常继承树
def show_hierarchy(exc_class: type, indent: int = 0) -> None:
print(" " * indent + exc_class.__name__)
for sub in exc_class.__subclasses__():
show_hierarchy(sub, indent + 1)
print("BaseException 继承树(前两层):")
# show_hierarchy(BaseException) # 取消注释查看完整树
print(f" BaseException")
for sub in BaseException.__subclasses__():
print(f" {sub.__name__} ({len(sub.__subclasses__())} 个子类)")性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| SyntaxError 检测 | O(n) | 解析源代码,n 为代码行数 |
| 异常抛出(未发生) | O(1) | try 块设置几乎零开销(Python 3.11+ 零成本异常) |
| 异常抛出(发生) | O(d) | d 为调用栈深度,需构建 traceback |
| isinstance 异常匹配 | O(m) | m 为 except 子句数量,线性检查 |
| 异常链回溯 | O(c) | c 为链中异常数量 |
Python 3.11+ 的零成本异常处理(Zero-Cost Exception Handling):
python
import dis
def example():
try:
x = 1
except ValueError:
pass
dis.dis(example)
# Python 3.11+ 输出:try 块内不再是 SETUP_FINALLY,而是用异常表
# 这意味着不抛出异常时 try 块完全零开销异常查找链路
异常传播性能路径:
┌─────────────────────────────────────────────────────────────┐
│ 异常时 vs 正常时的性能差异 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 正常路径(无异常): │
│ PUSH_EXC_INFO(设置异常处理器)→ 执行代码 │
│ → POP_EXC_INFO → 正常结束 │
│ 开销:~10ns(几乎可忽略) │
│ │
│ 异常路径(有异常): │
│ PUSH_EXC_INFO → 执行代码 → 抛出异常 │
│ → 遍历调用栈(O(d)) → 构建 traceback │
│ → 匹配 except 子句(O(m)) → 设置 __traceback__ │
│ → 执行 except 块 → 清理异常状态 │
│ 开销:~10µs - 数ms(取决于栈深度) │
│ │
│ 差距:异常路径比正常路径慢 1000-100000 倍 │
│ │
└─────────────────────────────────────────────────────────────┘设计动机
Python 为什么这样设计错误和异常体系?
| 设计选择 | 原因 | 替代方案对比 |
|---|---|---|
| 异常分为 BaseException 和 Exception | 系统级异常不应被业务代码捕获 | Java 的 Error vs Exception 类似 |
| 语法错误不可捕获 | 代码无法运行,捕获无意义 | 所有语言都如此 |
| 异常自动传播 | 不会被忽略,不处理就终止 | Go 需要每条路径检查 err |
| 异常是对象(携带丰富信息) | 方便调试和日志记录 | C 只用整数错误码 |
| 类型系统匹配异常 | isinstance 匹配支持继承 | C++ 和 Java 有类似机制 |
知识关联
错误与异常知识关联图:
┌───────────────┐
│ 解析器 │
│ 语法检查 │
└───────────────┘
│
↓
┌─────────────┐ ┌───────────────┐ ┌───────────────┐
│ 编译阶段 │────→│ 错误类型 │────→│ 运行阶段 │
│ SyntaxError│ │ 分类 │ │ Exception │
└─────────────┘ └───────────────┘ └───────────────┘
│
↓
┌───────────────┐
│ BaseException│
│ 继承体系 │
└───────────────┘
│
↓
┌───────────────┐
│ try-except │
│ 捕获机制 │
└───────────────┘
│
↓
┌───────────────┐
│ 自定义异常 │
│ raise + from │
└───────────────┘本章小结
┌─────────────────────────────────────────────────────────────┐
│ 错误与异常基础 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 错误分类: │
│ ✓ 语法错误:代码写错,无法运行 │
│ ✓ 运行时错误:运行时出现的异常 │
│ │
│ 常见异常: │
│ ✓ NameError:变量未定义 │
│ ✓ TypeError:类型错误 │
│ ✓ ValueError:值错误 │
│ ✓ IndexError:索引越界 │
│ ✓ KeyError:键不存在 │
│ ✓ ZeroDivisionError:除零错误 │
│ │
│ 处理方式: │
│ ✓ 语法错误:修正代码 │
│ ✓ 运行时异常:try-except 捕获 │
│ │
└─────────────────────────────────────────────────────────────┘