Skip to content

七、异常与文件 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 为根,其下主要分支有三:

  1. SystemExit:由 sys.exit() 触发,表示解释器请求退出;裸 except 会拦截它,导致程序无法正常退出。
  2. KeyboardInterrupt:用户按下 Ctrl+C 触发;裸 except 会静默吞掉中断信号,使程序无法响应终止。
  3. 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 虽能捕获,但不符合规范。

【自测】 以下代码执行后输出什么?

python
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 类型。

【自测】 以下代码分别抛出什么异常?

python
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 ​

【题目】输出结果是?

python
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。

【推导过程】

  1. 调用 f(),进入 try 块,执行到 return 1。
  2. 在 return 1 真正返回之前,解释器发现存在 finally 块,挂起当前返回值 1,先执行 finally 块。
  3. finally 块中执行 return 2,该 return 覆盖了之前挂起的 return 1。
  4. 函数最终返回 2,因此 print(f()) 输出 2。

【逐项辨析】

  • A. 1: 错误。try 中的 return 1 已被 finally 中的 return 覆盖,不会生效。
  • B. 2: 正确。finally 中的 return 具有最高优先级,覆盖 try 的返回值。
  • C. None: 错误。函数存在有效的 return 语句,不会返回 None。
  • D. 抛异常: 错误。代码语法合法,逻辑清晰,不会抛出异常。

【知识点】 Python 中 finally 块的执行机制遵循“必定执行”原则:

  1. 无论 try 块是正常结束、return、break、continue 还是抛异常,finally 都会执行。
  2. 若 finally 块中包含 return,则该 return 的返回值会覆盖 try 块或 except 块中的 return 值。
  3. 若 finally 块中抛出异常,则该异常会抑制 try 块中未处理的异常或 return 值。

这种特性使得 finally 块非常适合资源清理(关闭文件、释放锁等),但绝对不应在 finally 中写 return。在 finally 中 return 会隐藏 try 中的异常和正常返回值,导致调试极其困难,属于严重的代码坏味。

【记忆锚点】 finally 像霸道总裁:你说 return 1 要走?不行,我说 return 2 才算数!

【易混对比】

场景try 中有 returnfinally 中无 returnfinally 中有 return最终返回值
正常执行return 1执行清理—1
正常执行return 1—return 22(覆盖)
try 抛异常—执行清理return 22(掩盖异常)

换问法:若题目改为“若 finally 中没有 return,仅 print('cleanup'),输出结果是什么?”,则答案为 A(输出 1,并打印 cleanup)。

【自测】 以下代码输出什么?

python
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 语句的完整语法有三种常见形式:

  1. raise 异常类():raise ValueError("invalid input") —— 抛出新异常实例。
  2. raise 异常类:raise ValueError —— 自动调用无参构造,等价于 raise ValueError()。
  3. 裸 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(不跟任何异常对象)。

【自测】 以下代码执行后输出什么?

python
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 语句)。

【自测】 以下代码是否存在文件泄漏风险?为什么?

python
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+",若需追加写)。

【自测】 以下代码执行后文件内容是什么?

python
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';大文件逐行迭代。

持续学习,持续积累。