本章的两件事是「脚本从玩具变工具」的分界:文件读写(让数据活过一次运行)和异常处理(让程序体面地失败)。Python 的 with 语句和异常哲学(EAFP)都是全语言的招牌设计,Java 课第 12 章的 try-with-resources 在这里同源。
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 版)。
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 只在最外层兜底记日志。「异常的粒度」直接决定「能不能分情况处理」。
两派编程风格。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 风格的返回码检查)在风格上的分水岭之一。
自己的代码发现问题,抛异常(而不是返回 -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 的用异常,能预防的用检查」。
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 章验收)。
except Exception as e: print(e),最好接具体类型。def ask_int(prompt) 循环要求用户输入直到得到合法整数(ValueError 接住重问),体验 EAFP。NegativeNumberError,写 def sqrt_strict(n) 对负数输入抛出;调用方分别用「接住」和「不接住」两种方式跑,观察 traceback 的形状。