第 5 章 · 文件与异常

本章的两件事是「脚本从玩具变工具」的分界:文件读写(让数据活过一次运行)和异常处理(让程序体面地失败)。Python 的 with 语句和异常哲学(EAFP)都是全语言的招牌设计,Java 课第 12 章的 try-with-resources 在这里同源。

5.1 文件读写:with 是唯一正解

with open("notes.txt", "r", encoding="utf-8") as f:
    content = f.read()              # 全读
    # f.readline()                  # 一行(带换行符)
    # lines = f.readlines()         # 全部行(列表)

with open("out.txt", "w", encoding="utf-8") as f:   # "w" 覆盖写, "a" 追加
    f.write("第一行\n")
    f.write("第二行\n")

with open("notes.txt", encoding="utf-8") as f:      # 逐行遍历(内存友好)
    for line in f:
        print(line.rstrip())        # rstrip 去掉行尾换行

with(上下文管理器)是 Java 课 try-with-resources 的亲兄弟:块结束自动关闭文件(异常路径也不例外)。不用 with 的 open() + 手动 close() 是 bug 温床(忘关 → 句柄泄漏,Java 课第 12 章同款),新代码一律 with。逐行遍历(直接 for 文件对象)是处理大文件的正确姿势(惰性读,第 8 章的迭代器;readlines() 会把全文件装进内存)。

模式速记:r 读(默认)、w 写(覆盖!)、a 追加、rb/wb 二进制(第 12 章图片复制用)。encoding="utf-8" 必写(Windows 默认 GBK,跨平台乱码是 Python 第一报障,Java 课第 12 章的乱码案卷 Python 版)。

5.2 异常:出错了怎么办

Python 的错误以异常对象形式抛出,不接住则程序终止并打印 traceback(从下往上读:最后一行是病因,上面是调用链):

Traceback (most recent call last):
  File "main.py", line 5, in <module>
    print(1 / 0)
          ~^~
ZeroDivisionError: division by zero

接住异常:

try:
    n = int(input("输个整数:"))
    print(10 / n)
except ValueError:                        # 只接类型转换错
    print("那不是整数")
except ZeroDivisionError:                 # 只接除零
    print("不能除零")
except Exception as e:                    # 兜底(要窄后宽)
    print(f"出错了: {e}")
else:
    print("没出异常才会走这里")            # 可选
finally:
    print("无论如何都执行(清理)")         # 可选

和 Java 课第 8 章的差异:没有受检异常。Python 哲学里所有异常都是「运行时的」,编译器不逼你处理——自由换来的责任:异常文档要靠 docstring 写,吞异常(except: pass)是 Python 代码的第一恶习(错误无声消失,排查地狱)。

异常体系是一个类树:BaseException → Exception → ArithmeticError(→ ZeroDivisionError)、LookupError(→ KeyError/IndexError)、TypeError、ValueError……接具体类型(能精确处理),except Exception 只在最外层兜底记日志。「异常的粒度」直接决定「能不能分情况处理」。

5.3 EAFP:Python 的异常哲学

两派编程风格。LBYL(Look Before You Leap,三思而后行):先判断再做;

if "key" in d and d["key"] is not None:   # 先查
    use(d["key"])

EAFP(Easier to Ask Forgiveness than Permission,先斩后奏):直接做,错了接住:

try:
    use(d["key"])                          # 直接用
except KeyError:
    ...

Python 社区偏爱 EAFP:少一次查询、没有「检查和使用的间隙」(并发场景 LBYL 有 TOCTOU 竞态,操作系统课安全章节)、代码主路干净。多线程下 if key in d: d[key] 的检查和使用之间键可能被别的线程删掉,EAFP 一次原子完成。这是 Python 和 Java(C 风格的返回码检查)在风格上的分水岭之一。

5.4 抛异常与自定义异常

自己的代码发现问题,抛异常(而不是返回 -1/None 这类魔法值):

class InsufficientBalance(Exception):      # 继承 Exception
    """余额不足"""

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientBalance(f"余额 {balance},想取 {amount}")
    return balance - amount

try:
    withdraw(100, 500)
except InsufficientBalance as e:
    print(e)                               # 余额 100,想取 500

raise 抛出(Java 的 throw),自定义异常继承 Exception 带消息(Java 课第 8 章同款设计)。设计准则:异常用于「异常情况」(违反契约、外部失败),可预期的正常分支别用(用返回值/默认值,否则控制流难读);「能 recovery 的用异常,能预防的用检查」。

5.5 标准库三件套:sys、os、pathlib

import sys
sys.argv              # 命令行参数列表(argv[0] 是脚本名,和 C 的 main 参数同款)
sys.exit(1)           # 退出码(0 成功,非 0 失败,shell 能感知)

from pathlib import Path
p = Path("data") / "2026" / "log.txt"    # 路径拼接(跨平台 /)
p.exists()            # 存在吗
p.read_text(encoding="utf-8")            # 一行读完小文件
p.write_text("hi")                       # 一行写出
list(Path(".").glob("*.py"))             # 通配符列文件

pathlib 是现代路径操作的标准答案(老的 os.path 字符串拼接已过时);第 10 章实战大量使用。命令行参数(sys.argv)+ 文件读写 + 异常处理,已经够写正经的命令行工具了(第 10 章验收)。

常见错误

  • 忘写 encoding。Windows 上读 UTF-8 文件乱码;「open 必带 encoding」写进肌肉记忆。
  • "w" 模式覆盖。open("log.txt", "w") 一跑,老日志全没了;追加用 "a"。
  • 裸 except / except: pass。吞掉一切错误;至少 except Exception as e: print(e),最好接具体类型。
  • 在 with 块外用 f。文件已关,读出 ValueError: I/O operation on closed file;把读取放块内。

练习

  1. 动手:写一个日志分析器:读一个文本日志(自己造几行「ERROR xxx / INFO xxx」),统计 ERROR 行数并提取出 ERROR 行内容,写入 errors.txt。全程 with + utf-8。
  2. 动手:健壮的输入函数:def ask_int(prompt) 循环要求用户输入直到得到合法整数(ValueError 接住重问),体验 EAFP。
  3. 实现:自定义异常 NegativeNumberError,写 def sqrt_strict(n) 对负数输入抛出;调用方分别用「接住」和「不接住」两种方式跑,观察 traceback 的形状。
  4. 思考:Python 的 EAFP 和 Java 课第 8 章「受检异常强制处理」是两种极端。从「代码量、错误处理完整性、并发安全」三个角度对比,再想:动态类型语言为什么天然倾向 EAFP?(提示:没有编译器帮你查「是否处理」,风格必须自己立。)