七、异常与文件 IO(P61–P66)
P61 · 知识点:异常捕获范围
【题目】以下哪个写法可以捕获所有常规异常?
A. except: B. except Exception: C. try ... finally D. A 和 B 都可以
答案:D
【考点】裸 except 与 except Exception 的捕获范围。
【结论】选 D;裸 except 与 except Exception 均可捕获常规程序异常,但二者捕获范围与规范性存在差异。
【逐项辨析】
- A. except: 裸 except 捕获一切继承自 BaseException 的异常,自然包含 Exception 子树下的所有常规异常,故能捕获常规异常,但范围过宽,可能误吞 SystemExit、KeyboardInterrupt 等非预期异常。
- B. except Exception: 仅捕获 Exception 及其子类,覆盖所有常规程序错误(ValueError、TypeError、IOError 等),不会拦截系统级异常,写法更规范。
- C. try ... finally: finally 仅保证清理代码执行,不具备捕获异常的能力,异常仍会向外传播。
- D. A 和 B 都可以: 正确。二者均能捕获常规异常,区别在于范围宽窄与工程规范。
【知识点】 Python 异常体系以 BaseException 为根,其下主要分支有三:
- SystemExit:由 sys.exit() 触发,表示解释器请求退出;裸 except 会拦截它,导致程序无法正常退出。
- KeyboardInterrupt:用户按下 Ctrl+C 触发;裸 except 会静默吞掉中断信号,使程序无法响应终止。
- Exception:所有内置与用户自定义常规异常的基类,涵盖 ValueError、TypeError、KeyError、IndexError、OSError 等。
在工程实践中,严禁使用裸 except,推荐显式书写 except Exception: 或更具体的异常类型。若需同时捕获多个异常,可写成 except (TypeError, ValueError): 的元组形式。PEP 8 亦建议避免裸 except,以保障程序的可中断性与可维护性。
【记忆锚点】 裸 except 像大网捞鱼,好鱼坏鱼全捞走;Exception 像细网筛,只留程序错,不拦系统流。
【易混对比】
| 写法 | 捕获范围 | 是否拦截 SystemExit / Ctrl+C | 推荐度 |
|---|---|---|---|
| except: | BaseException 全部子类 | 是 | 不推荐 |
| except Exception: | Exception 全部子类 | 否 | 推荐 |
| except (A, B): | 指定的多个异常 | 否 | 最推荐(精确捕获) |
换问法:若题目改为“哪种写法最规范地捕获所有常规异常?”,则答案变为 B,因为裸 except 虽能捕获,但不符合规范。
【自测】 以下代码执行后输出什么?
try:
raise KeyboardInterrupt
except:
print("caught")答:输出 caught。因为裸 except 捕获了 KeyboardInterrupt,导致用户无法通过 Ctrl+C 中断程序。若将 except: 改为 except Exception:,则 KeyboardInterrupt 会向上传播,程序终止。
与第 P62 题连考。
【知识关联】
- 同库关联:与 P62–P64、J45–J47(Java 异常)对照。
- 实现层:except 从上到下匹配;只捕获匹配类型。
- 面试追问:① 为何先子类后父类?④ 捕获过多风险?
【拓展延伸】
- 变式问法:
except (A, B);except Exception as e。 - 版本差异:3.x except 语法。
- 工程注意点:不要裸 except: 吞一切;记录类型与栈。
P62 · 知识点:常见异常类型
【题目】执行 int("abc") 抛出?
A. ValueError B. TypeError C. KeyError D. IndexError
答案:A
【考点】常见内建异常类型辨析。
【结论】选 A;字符串 "abc" 无法被解析为整数,属于“值的格式不合法”,触发 ValueError。
【逐项辨析】
- A. ValueError: 正确。int() 函数要求字符串内容是合法的整数字面量,"abc" 格式不符,抛出 ValueError。
- B. TypeError: 错误。TypeError 发生在操作或函数应用于不适当类型的对象时,例如 "a" + 1(字符串与整数相加),而此处传入的确实是 str 类型,int() 接受 str,只是值不合法。
- C. KeyError: 错误。KeyError 仅在使用字典时访问不存在的键触发,如 d["x"] 且 "x" 不在字典中。
- D. IndexError: 错误。IndexError 发生在序列下标越界时,如 lst[5] 而 lst 长度不足 6。
【知识点】 Python 内建异常中,ValueError 与 TypeError 最容易混淆,核心区分逻辑如下:
- TypeError:“我给你一个苹果,你却要我用它当锤子”——类型不匹配,函数或操作根本不接受该类型。
- ValueError:“你给我一个苹果,形状也对,但它烂了不能吃”——类型正确,但值的内容或格式不符合要求。
其他常见内建异常触发场景:
- KeyError:字典中键不存在。
- IndexError:列表、元组等序列索引超出范围。
- AttributeError:对象没有该属性或方法。
- ZeroDivisionError:除数为零。
- FileNotFoundError:打开不存在的文件(OSError 子类)。
int() 的签名为 int(x, base=10),当 x 为字符串时,base 指定进制,字符串必须是该进制下的合法数字表示,否则一律 ValueError。
【记忆锚点】 Type 是“类型不对”,Value 是“值太废”;字典找键 KeyError,列表越界 IndexError。
【易混对比】
| 异常 | 触发场景 | 典型示例 |
|---|---|---|
| ValueError | 类型对,但值不合法 | int("abc"), int("10.5") |
| TypeError | 类型不对,操作无法进行 | "a" + 1, len(42) |
| KeyError | 字典键不存在 | d = {}; d["x"] |
| IndexError | 序列索引越界 | [1][5] |
换问法:若题目改为“执行 len(42) 抛出?”,答案为 B(TypeError),因为 len() 不接受 int 类型。
【自测】 以下代码分别抛出什么异常?
d = {"a": 1}
print(d["b"]) # ?
print([1, 2][5]) # ?
print("x" + 1) # ?答:第一行抛出 KeyError(字典键 "b" 不存在);第二行抛出 IndexError(索引 5 越界);第三行抛出 TypeError(字符串与整数不能相加)。
与第 P63 题连考。
【知识关联】
- 同库关联:与 P61;常见类型 ValueError/TypeError/KeyError/IndexError。
- 实现层:BaseException→Exception→具体;KeyboardInterrupt/SystemExit 通常不捕。
- 面试追问:① KeyError 与 IndexError 场景?② 何时 raise?
【拓展延伸】
- 变式问法:
int('a')抛什么;空列表 pop。 - 版本差异:语义稳定。
- 工程注意点:业务异常自定义 Exception 子类;携带上下文。
P63 · 知识点:finally 与 return
【题目】输出结果是?
def f():
try:
return 1
finally:
return 2
print(f())A. 1 B. 2 C. None D. 抛异常
答案:B
【考点】finally 在 return 之前执行,finally 中的 return 覆盖 try 的 return。
【结论】选 B;finally 块中的 return 会覆盖 try 块中的 return,函数最终返回 2。
【推导过程】
- 调用 f(),进入 try 块,执行到 return 1。
- 在 return 1 真正返回之前,解释器发现存在 finally 块,挂起当前返回值 1,先执行 finally 块。
- finally 块中执行 return 2,该 return 覆盖了之前挂起的 return 1。
- 函数最终返回 2,因此 print(f()) 输出 2。
【逐项辨析】
- A. 1: 错误。try 中的 return 1 已被 finally 中的 return 覆盖,不会生效。
- B. 2: 正确。finally 中的 return 具有最高优先级,覆盖 try 的返回值。
- C. None: 错误。函数存在有效的 return 语句,不会返回 None。
- D. 抛异常: 错误。代码语法合法,逻辑清晰,不会抛出异常。
【知识点】 Python 中 finally 块的执行机制遵循“必定执行”原则:
- 无论 try 块是正常结束、return、break、continue 还是抛异常,finally 都会执行。
- 若 finally 块中包含 return,则该 return 的返回值会覆盖 try 块或 except 块中的 return 值。
- 若 finally 块中抛出异常,则该异常会抑制 try 块中未处理的异常或 return 值。
这种特性使得 finally 块非常适合资源清理(关闭文件、释放锁等),但绝对不应在 finally 中写 return。在 finally 中 return 会隐藏 try 中的异常和正常返回值,导致调试极其困难,属于严重的代码坏味。
【记忆锚点】 finally 像霸道总裁:你说 return 1 要走?不行,我说 return 2 才算数!
【易混对比】
| 场景 | try 中有 return | finally 中无 return | finally 中有 return | 最终返回值 |
|---|---|---|---|---|
| 正常执行 | return 1 | 执行清理 | — | 1 |
| 正常执行 | return 1 | — | return 2 | 2(覆盖) |
| try 抛异常 | — | 执行清理 | return 2 | 2(掩盖异常) |
换问法:若题目改为“若 finally 中没有 return,仅 print('cleanup'),输出结果是什么?”,则答案为 A(输出 1,并打印 cleanup)。
【自测】 以下代码输出什么?
def g():
try:
return 10
finally:
print("cleanup")
raise ValueError("oops")
print(g())答:先打印 cleanup,然后抛出 ValueError("oops"),程序因未捕获异常而终止,不会打印任何返回值。因为 finally 中抛出的异常会覆盖 try 中挂起的 return 10。
与第 P64 题连考。
【知识关联】
- 同库关联:与 J46(Java finally 与 return)对照;with 与 try/finally。
- 实现层:finally 在 return 前执行;finally 中 return 会覆盖。
- 面试追问:① finally 是否总是执行?② 与 with 关系?
【拓展延伸】
- 变式问法:try return 后 finally 打印顺序。
- 版本差异:语义稳定。
- 工程注意点:资源用 with;finally 不写 return。
P64 · 知识点:raise
【题目】raise 语句的作用是?
A. 捕获异常 B. 主动抛出异常 C. 忽略异常 D. 记录异常日志
答案:B
【考点】raise 抛出异常,try/except 捕获异常。
【结论】选 B;raise 用于主动抛出异常,将错误条件显式传递给调用方。
【逐项辨析】
- A. 捕获异常: 错误。捕获异常由 try/except 语句完成,raise 不具备捕获功能。
- B. 主动抛出异常: 正确。raise 可抛出指定异常实例(raise ValueError("msg")),或在 except 块中裸 raise 以重新抛出当前异常。
- C. 忽略异常: 错误。忽略异常通常指 except 块中不写任何处理逻辑(空 except),这与 raise 的作用完全相反。
- D. 记录异常日志: 错误。记录日志由 logging 模块完成,raise 仅负责中断当前控制流并传播异常。
【知识点】 raise 语句的完整语法有三种常见形式:
- raise 异常类():raise ValueError("invalid input") —— 抛出新异常实例。
- raise 异常类:raise ValueError —— 自动调用无参构造,等价于 raise ValueError()。
- 裸 raise:在 except 块中单独写 raise —— 重新抛出当前正在处理的异常,保留原始堆栈信息,是最干净的异常转发方式。
从异常处理链来看:
- 下层代码(被调用方)通过 raise 报告“我出错了”。
- 上层代码(调用方)通过 try/except 决定“我来处理”。
- 若上层不处理,异常继续向上冒泡,直至被捕获或导致程序终止。
此外,Python 3 中 raise 支持异常链语法 raise NewException from old_exception,用于在转换异常类型时保留原始异常信息,便于调试。
【记忆锚点】 raise 是“扔炸弹”,把问题丢出去;except 是“拆炸弹”,把问题接下来。
【易混对比】
| 语句 | 作用 | 方向 | 示例 |
|---|---|---|---|
| raise | 抛出/重新抛出异常 | 向上传播 | raise ValueError("bad") |
| try/except | 捕获并处理异常 | 向下拦截 | except ValueError: ... |
| assert | 条件为假时抛 AssertionError | 向上传播 | assert x > 0 |
| logging.error | 记录日志,不中断程序 | 无 | logging.error("msg") |
换问法:若题目改为“在 except 块中如何保留原始堆栈重新抛出异常?”,答案应使用裸 raise(不跟任何异常对象)。
【自测】 以下代码执行后输出什么?
try:
raise KeyError("missing")
except KeyError:
print("caught")
raise答:先打印 caught,然后抛出 KeyError("missing") 并终止程序(因为 raise 重新抛出了异常,且外层无 except 捕获)。裸 raise 保留了原始异常对象和堆栈信息。
与第 P65 题连考。
【知识关联】
- 同库关联:与 P61/P62、J48(throw/throws)对照。
- 实现层:raise 抛出;
raise X from Y链式原因。 - 面试追问:① 如何保留原始异常?② 重新抛出?
【拓展延伸】
- 变式问法:
raise ValueError('x');raise裸抛。 - 版本差异:3.x 异常链。
- 工程注意点:包装底层异常用 from;避免吞掉 cause。
P65 · 知识点:with 语句
【题目】打开文件并确保使用后自动关闭(即使发生异常),推荐方式?
A. f = open("a.txt"); ...; f.close() B. with open("a.txt") as f: ... C. open("a.txt", auto_close=True) D. A 和 B 效果相同,都推荐
答案:B
【考点】with 语句与上下文管理器(enter/exit)自动资源管理。
【结论】选 B;with 语句通过上下文管理器确保资源在使用后自动释放,是文件操作的标准推荐写法。
【逐项辨析】
- A. f = open("a.txt"); ...; f.close(): 错误。手动 close 若中间代码抛异常,close() 可能不被执行,导致文件句柄泄漏;且写法冗长,需配合 try/finally 才能保证安全。
- B. with open("a.txt") as f: ...: 正确。with 语句在代码块结束时(正常结束或抛异常)自动调用 f.close(),简洁且安全,是 PEP 343 引入的标准做法。
- C. open("a.txt", auto_close=True): 错误。Python 内置 open() 函数不存在 auto_close 参数,该选项为干扰项。
- D. A 和 B 效果相同,都推荐: 错误。A 与 B 仅在“中间代码无异常”时效果相同,但异常场景下 A 无法保证关闭,故 B 才是推荐做法。
【知识点】 with 语句背后的核心机制是上下文管理器协议,要求对象实现以下两个魔术方法:
- enter(self):进入 with 块时调用,返回值绑定到 as 后的变量。
- exit(self, exc_type, exc_val, exc_tb):离开 with 块时调用,无论是否正常退出;接收三个参数描述可能存在的异常信息,若返回 True 则抑制异常,否则异常继续传播。
文件对象 file object 原生支持上下文管理器协议,因此可直接用于 with。除文件外,threading.Lock、socket、数据库连接等也普遍支持 with,统一了资源获取即初始化(RAII)的编程模式。
从字节码层面看,with 语句会被编译为类似 try/finally 的结构,确保 exit 必定执行,但源码层面更简洁、语义更清晰。
【记忆锚点】 with 像自动门:人走门自动关,不管他是笑着走还是摔着走。
【易混对比】
| 方式 | 异常时是否关闭 | 代码量 | 推荐度 |
|---|---|---|---|
| 手动 open/close | 否(除非包 try/finally) | 多 | 不推荐 |
| try/finally | 是 | 较多 | 可用但冗长 |
| with | 是 | 少 | 强烈推荐 |
换问法:若题目改为“以下哪种写法在发生异常时一定会关闭文件?”,答案仍为 B(with 语句)。
【自测】 以下代码是否存在文件泄漏风险?为什么?
def read_data(path):
f = open(path)
return f.read()答:存在泄漏风险。若 open 成功但 read 抛异常,函数直接退出,f.close() 从未被调用,文件句柄无法释放。应改为 with open(path) as f: return f.read()。
与第 P66 题连考。
【知识关联】
- 同库关联:与 J50(try-with-resources)对照;
__enter__/__exit__。 - 实现层:上下文管理器协议;contextlib.contextmanager 语法糖。
- 面试追问:① 如何自定义 with 对象?② 异常在 exit 传播?
【拓展延伸】
- 变式问法:多上下文
with a() as x, b() as y:;contextlib.ExitStack。 - 版本差异:语义稳定;异步 async with。
- 工程注意点:文件/锁/连接一律 with;不要手写 try/finally 开关资源。
P66 · 知识点:文件打开模式
【题目】以追加方式打开文件(不清空原有内容,从末尾写入)的模式是? A. "w" B. "a" C. "r" D. "x"
答案:B
【考点】文件打开模式表:r/w/a/x/b/+。
【结论】选 B;模式 "a" 表示 append(追加),写入时指针位于文件末尾,不清空原有内容。
【逐项辨析】
- A. "w": 错误。"w" 为写入模式,打开时立即清空文件全部内容,从头开始写入;若文件不存在则创建新文件。
- B. "a": 正确。"a" 为追加模式,写入指针始终位于文件末尾,原有内容完整保留;若文件不存在则创建新文件。
- C. "r": 错误。"r" 为只读模式(默认模式),文件不存在时抛出 FileNotFoundError,且不能写入。
- D. "x": 错误。"x" 为独占创建模式,仅当文件不存在时创建并打开用于写入;若文件已存在则抛出 FileExistsError,不能用于向已有文件追加内容。
【知识点】 Python open() 函数的 mode 参数是一个字符串,由以下字符组合而成:
基础模式(必选其一):
- "r":只读,文件指针在开头。文件不存在则报错。
- "w":只写,文件指针在开头,截断文件为 0 长度(清空)。不存在则创建。
- "a":追加写,文件指针在末尾。不存在则创建。
- "x":独占创建写,文件必须不存在,否则报错。
修饰模式(可与基础模式组合):
- "b":二进制模式(rb、wb、ab),用于图片、音频等非文本数据。
- "+":读写模式(r+、w+、a+),在基础模式上增加反向操作权限。注意 w+ 仍然会清空文件。
追加模式的典型应用场景包括日志文件写入、数据累积记录等,其核心优势在于“写操作不会影响历史数据”。需要注意的是,在 "a" 模式下,seek() 操作虽然可以改变文件指针位置,但写入时指针仍会被强制重置到文件末尾(部分平台行为一致,但不可依赖 seek 后写入到文件中间)。
【记忆锚点】 r 读 w 写 a 追 x 独占不冲突;b 二进制 + 读写两不误。
【易混对比】
| 模式 | 文件不存在时 | 是否清空原有内容 | 写入位置 | 主要用途 |
|---|---|---|---|---|
| "r" | 报错 | 否(只读) | 开头 | 读取配置、文本 |
| "w" | 创建新文件 | 是(截断) | 开头 | 覆盖写入、生成新文件 |
| "a" | 创建新文件 | 否 | 末尾 | 日志追加、累积数据 |
| "x" | 创建新文件 | — | 开头 | 安全创建(避免覆盖) |
| "r+" | 报错 | 否 | 开头 | 读写已有文件 |
| "w+" | 创建新文件 | 是(截断) | 开头 | 读写并覆盖 |
换问法:若题目改为“要以读写方式打开已有文件且不截断内容,应使用哪个模式?”,答案为 "r+"(或 "a+",若需追加写)。
【自测】 以下代码执行后文件内容是什么?
with open("data.txt", "w") as f:
f.write("hello")
with open("data.txt", "a") as f:
f.write(" world")
with open("data.txt", "r") as f:
print(f.read())答:输出 hello world。第一行以 "w" 模式写入 hello,清空原有内容;第二行以 "a" 模式追加 world;第三行读取并打印完整内容。
与第 P61 题连考。
【知识关联】
- 同库关联:与 P65;模式 r/w/a/b/+。
- 实现层:底层 OS 文件描述符;文本默认编码随系统/配置。
- 面试追问:① w 会截断吗?② 如何指定编码?
【拓展延伸】
- 变式问法:
open(path,'r',encoding='utf-8');二进制 rb。 - 版本差异:Py3 默认文本模式是 unicode。
- 工程注意点:显式 encoding='utf-8';大文件逐行迭代。