PY-06 文件与异常
06 · 文件与异常(File & Exception)
📅 预计 60 分钟 | ⭐ = 高频考点
✍️ 本讲配套练习:本地python grader.py 06,或网页练习
6.1 文件是什么:把数据存进硬盘的笔记本
程序跑在内存里,而内存有个特点:断电即失。你算了一下午的成绩单,关掉程序就没了——这当然不行。文件(file)就是程序把数据写到硬盘上的方式,相当于一本可以长期保存的笔记本:程序把它「打开」,在上面写字或读字,用完了再「合上」。下次开机,内容还在。
Python 操作文件的流程永远是三步:
- 打开:
open()打开文件,像翻开笔记本。 - 读写:用
write()写、用read()读。 - 关闭:
close()合上笔记本,把内容真正落到硬盘。
最安全的写法是用 with 语句(也叫上下文管理器)。它像一个「细心的书童」:用完文件自动帮你合上,即使中途出错也会合上,你不用手动 close。with 是更规范也更常见的写法。
# 写文件:打开 → 写入 → 自动关闭
with open("data.txt", "w", encoding="utf-8") as f:
f.write("第一行\n")
f.write("第二行\n")
# 到这里文件已自动关闭,再打开读回来
with open("data.txt", "r", encoding="utf-8") as f:
print(f.read())
# 输出:第一行
# 第二行
⚠️ 常见错误
- 裸用
f = open(...)却不close():数据可能还躺在内存里没落盘,程序一结束就丢了。 - 用
"w"去读文件:"w"会先清空再写,原内容直接没了。 - 漏写
encoding="utf-8":写中文时可能报UnicodeEncodeError或存成乱码。
6.2 打开模式:你怎么使用这本笔记本
open(path, mode, encoding=...) 的第二个参数是模式,决定你是「看」还是「写」;写的话,是从第 0 页开始,还是在最后一页接着写。
| 模式 | 含义 | 文件不存在时 |
|---|---|---|
"r" |
只读(默认) | 报错 FileNotFoundError |
"w" |
写,清空覆盖 | 创建 |
"a" |
追加,在末尾接着写 | 创建 |
"r+" / "w+" |
读写 | r+ 报错 / w+ 创建 |
生活比喻:"r" 是只看不写;"w" 是撕掉旧本子重写一页;"a" 是在最后一页的末尾接着写,旧内容全保留。
# a 模式追加:每次运行都在末尾加一行
with open("log.txt", "a", encoding="utf-8") as f:
f.write("2026-08-02 登录成功\n")
# 第一次运行后文件内容:
# 2026-08-02 登录成功
# 第二次运行后:
# 2026-08-02 登录成功
# 2026-08-02 登录成功
⚠️ 常见错误
- 想追加却用
"w":每次运行都把旧记录清空,日志只剩最后一条。 - 用
"r+"读不存在的文件:直接抛FileNotFoundError,程序崩溃。要「先判断再读」见 6.7。
6.3 读文件的四种姿势
拿到文件对象 f 后,读内容有四种方式,用途不同:
# 1) read():一口气全读成一个大字符串
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read() # "第一行\n第二行\n"
# 2) readline():只读一行
with open("data.txt", "r", encoding="utf-8") as f:
line1 = f.readline() # "第一行\n"
line2 = f.readline() # "第二行\n"
# 3) readlines():读出所有行,每行是列表的一个元素
with open("data.txt", "r", encoding="utf-8") as f:
lines = f.readlines() # ["第一行\n", "第二行\n"]
# 4) for line in f:逐行迭代,最省内存 ⭐
with open("data.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip()) # strip() 去掉末尾换行
为什么「for line in f」最省内存?因为 read() 会把整个文件装进内存——文件有 1 个 G,内存就占 1 个 G;而 for 循环是读一行、处理一行,内存里始终只有一行。处理大文件(比如大数据课的日志)一定要用 for 迭代。
readlines() 返回的每行都带 \n,打印前记得 .strip(),否则会多出空行。
⚠️ 常见错误
- 忘了
.strip():print 出来好像多了一行空行。 - 大文件用
read():内存直接爆掉(大数据课程第一课就强调这个)。 - 一个
read()之后又调read():第二次得到空字符串——因为指针已经到末尾了(见 6.5)。
6.4 写文件的两种姿势
写也有两个方法:write() 一次写一个字符串,writelines() 一次写一个字符串列表。
# write():写单个字符串,换行要自己加 \n
with open("a.txt", "w", encoding="utf-8") as f:
f.write("第一行\n")
f.write("第二行\n")
# writelines():一次写一个列表,列表里每个元素要自带 \n
lines = ["甲\n", "乙\n", "丙\n"]
with open("b.txt", "w", encoding="utf-8") as f:
f.writelines(lines)
# 读回来验证
with open("b.txt", "r", encoding="utf-8") as f:
print(f.readlines()) # ['甲\n', '乙\n', '丙\n']
⚠️ 常见错误
writelines忘了给每个元素加\n:所有内容挤成一行。- 以为
write会自动换行:不会,你必须自己写\n。
6.5 文件指针 seek:读到哪里了
文件对象内部有个「指针」,记录你读到哪。read() 读完,指针停在末尾,再读就是空。seek(位置) 可以把指针挪回任意位置。
# 先造一个文件
with open("seek.txt", "w", encoding="utf-8") as f:
f.write("abcdef")
# 把指针挪到下标 2,再读
with open("seek.txt", "r", encoding="utf-8") as f:
f.seek(2) # 指针跳到第三个字符
print(f.read()) # cdef
seek 的起点从 0 开始,f.seek(0) 就是回到文件开头。
⚠️ 常见错误
- 忘了指针已经到末尾:连续两次
read(),第二次是空字符串。 - 想重新读却不再 open:用
f.seek(0)回到开头,或干脆重新打开。
6.6 文件编码:为什么中文会乱码
电脑只存 0 和 1,「中」这个字怎么对应到 0/1,要靠编码表(encoding)。Python 3 默认用 UTF-8,这是目前最通用的编码。如果文件是别的编码,读出来就是乱码,甚至报 UnicodeDecodeError。
# 读中文文件:务必显式指定 encoding="utf-8"
with open("names.txt", "r", encoding="utf-8") as f:
print(f.readline()) # 小明
写 CSV 给 Excel 用时,常用 utf-8-sig(带 BOM),这样 Excel 打开不乱码。
⚠️ 常见错误
- 读写都忘写
encoding="utf-8":Windows 上可能默认用 GBK,中文全变乱码。 - 用
utf-8读utf-8-sig写的文件:第一行开头多个看不见的。
6.7 文件存在判断:动手前先问一问
读一个不存在的文件会直接抛 FileNotFoundError 让程序崩溃。写程序前先判断文件在不在,是常见的防御式写法。
import os
if os.path.exists("data.txt"): # 文件或目录是否存在
print("文件在")
else:
print("文件不存在")
os.remove("data.txt") # 删除文件
os.path.isdir("06-文件与异常") # 判断是不是目录
也可以不判断,让它抛异常再用 try 接住(见 6.9)——两种思路都行。
⚠️ 常见错误
- 直接
open不判断:程序一遇缺失文件就崩。 - 忘了
import os:直接NameError: name 'os' is not defined。
6.8 CSV 与 JSON:两种实用的数据格式
CSV(表格数据)
CSV 是「逗号分隔值」,Excel 和数据分析工具都能直接打开。Python 课的大作业常要求用它存成绩表。
import csv
# 写 CSV:newline="" 避免 Windows 下出现多余空行
with open("scores.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["姓名", "成绩"]) # 表头
w.writerow(["小明", 90])
w.writerow(["小红", 85])
# 读 CSV:每行是一个列表
with open("scores.csv", "r", encoding="utf-8-sig") as f:
rows = list(csv.reader(f)) # [['姓名','成绩'], ['小明','90'], ['小红','85']]
print(rows[1][0]) # 小明
# 推荐 DictReader:把第一行当表头,按名字取值 ⭐
with open("scores.csv", "r", encoding="utf-8-sig") as f:
for row in csv.DictReader(f):
print(row["姓名"], row["成绩"]) # 小明 90 / 小红 85
JSON(结构化数据)
JSON 是「JavaScript 对象表示」,长得和 Python 字典几乎一样,是网页前后端交换数据的标准格式。
import json
data = {"name": "小明", "scores": [90, 85]}
# 写入:ensure_ascii=False 让中文以原文存储,而不是 \uXXXX
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
# 读取
with open("data.json", "r", encoding="utf-8") as f:
obj = json.load(f)
print(obj["name"]) # 小明
# 字符串 <-> 对象:网络传输用
s = json.dumps(data, ensure_ascii=False) # 字典 -> 字符串
back = json.loads(s) # 字符串 -> 字典
记住 JSON 四件套:dump 字典→文件,load 文件→字典,dumps 字典→字符串,loads 字符串→字典。多一个 s 就是 string(字符串)。
⚠️ 常见错误
dump忘写ensure_ascii=False:文件里全是小明,人看不出来是中文。- 读 JSON 忘写
encoding="utf-8":中文乱码。 json.load遇到空文件:抛JSONDecodeError。
6.9 异常:程序出状况时的应急预案
异常(exception)是程序运行到「意料之外」时发出的信号。比如用户输入了字母你却要转整数、除以 0、读不存在的文件——Python 都会抛异常。如果不处理,程序就崩。try/except 就是应急预案:先试着跑,出问题就按预案处理,程序不崩。
try:
num = int(input("输入数字:"))
result = 100 / num
print(result)
except ValueError:
print("那不是数字!")
except ZeroDivisionError:
print("不能除以 0")
except Exception as e: # 兜底:抓其他所有异常,尽量别滥用
print("出错了:", e)
else:
print("没出错才执行这块")
finally:
print("无论怎样都会执行")
执行顺序:
try里的代码先跑。- 中途抛异常 → 跳到第一个匹配的
except,执行完继续往下走(try里剩下的代码不再执行)。 - 没抛异常 → 执行
else(可选)。 - 不管有没有异常 → 执行
finally(可选),常用来关闭资源。
⚠️ 常见错误
- 空
except::把所有错误都吞了,出问题你完全不知道,程序带病运行。 - 用
except Exception当万能兜底还不看类型:问题被掩盖,调试无从下手。 - 在 except 里不处理、不记录、不打印:异常被静默吞掉,最隐蔽的 bug 之一。
6.10 一个 except 抓多种异常
不同错误对应不同异常类型。考试常考「这个操作抛什么异常」,先背熟:
int("abc") # ValueError 值不合法
1 / 0 # ZeroDivisionError 除零
lst = [1, 2, 3]
lst[5] # IndexError 下标越界
d = {"a": 1}
d["b"] # KeyError 键不存在
"a" + 1 # TypeError 类型不匹配
open("no.txt") # FileNotFoundError 文件不存在
一个 except 可以同时抓多种异常:
try:
x = int("abc") + "def"
except (ValueError, TypeError):
print("类型或值有问题")
异常还有继承体系:ZeroDivisionError 是 ArithmeticError 的子类,而 ArithmeticError 又是 Exception 的子类。抓父类能抓到所有子类——所以 except Exception 什么都能抓,这也是它危险的原因。
⚠️ 常见错误
- 异常类型拼错:写了
except ValueError:,结果真异常是TypeError,根本没接住,程序照样崩。 - 抓得太宽(
except Exception)不看具体类型:把所有问题混为一谈,难定位。
6.11 主动抛异常:raise 与自定义异常
有时不是 Python 报错,而是业务规则不允许。比如成绩不能是负数、年龄不能是 500。这时可以用 raise 主动抛异常,让调用方统一处理。
def set_age(age):
if age < 0 or age > 150:
raise ValueError("年龄不合法")
return age
set_age(200) # 抛 ValueError: 年龄不合法
还可以定义自己的异常类型,让错误信息更有语义:
class ScoreError(Exception):
"""成绩不合法时抛出"""
pass
def check_score(score):
if score < 0 or score > 100:
raise ScoreError("成绩超出 0-100")
return score
try:
check_score(-5)
except ScoreError as e:
print(e) # 成绩超出 0-100
raise ... from ...:当你在异常处理里抛新异常时,用 from 把原始异常「挂」在后面,不丢根因——排错时能一路追到最初的错误。
def load_cfg(path):
try:
with open(path, encoding="utf-8") as f:
return f.readline()
except FileNotFoundError as e:
raise ValueError("配置文件缺失") from e
⚠️ 常见错误
- 在 except 里抛新异常但不
from e:原始错误信息丢失,排错困难。 - 自定义异常类忘了继承
Exception:except ScoreError根本接不住。
6.12 综合示例:读取成绩文件并容错
把文件读取和异常处理结合,是这门课的常见题型。下面这个函数读取成绩文件,每行一个成绩,遇到空行跳过,遇到非法内容记一笔,最后返回统计。它也是 ex15 的参考答案思路。
def analyze_scores(path):
total = 0
count = 0
bad = 0
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line: # 空行跳过
continue
try:
total += int(line)
count += 1
except ValueError: # 非法成绩,记一笔,程序不崩
bad += 1
return {"sum": total, "count": count, "bad": bad}
# 假设 scores.txt 内容是:90\nabc\n\n85\n
print(analyze_scores("scores.txt"))
# 输出:{'sum': 175, 'count': 2, 'bad': 1}
⚠️ 常见错误
int(line)不包 try:一行数据坏了,整个文件读不了,程序直接崩。- 把
FileNotFoundError也吞进循环里:逻辑混乱,定位困难。
💡 记忆口诀
- 文件三连:打开 → 读写 → 关闭;
with帮你关,最省心。 - 模式别搞混:
r读、w清空写、a追加。 - 大文件循环读:
for line in f最省内存。 - 编码不写就乱码:
encoding="utf-8"忘一次,乱码跟你走。 - 异常五件套:try 试、except 接、else 无错、finally 必清、raise 主动抛。
- JSON 四件套:dump 存文件、load 取文件、dumps 转字符串、loads 转对象。
📌 双语术语表(本讲)
| 中文 | English |
|---|---|
| 文件读写 | file I/O |
| 只读 / 写入 / 追加 | read / write / append |
| 上下文管理器 | context manager(with) |
| 文件指针 | file pointer / seek |
| 编码 | encoding |
| 异常 | exception |
| 抛出 | raise |
| 捕获 | catch |
| 清理 | finally |
| 解析 / 序列化 | parse / serialize |
| 自定义异常 | custom exception |
| 存在性检查 | existence check |
⭐ 本讲考点清单
with open(...) as f+encoding="utf-8""r"只读、"w"清空、"a"追加- 读取三件套:
read()/readline()/readlines();大文件用for line in f write()与writelines()(记得加\n)seek(0)回到开头 / 文件指针位置- 文件编码 UTF-8 与乱码原因
os.path.exists/os.remove文件存在判断- CSV 读写(
csv.reader/DictReader/utf-8-sig) - JSON
dump/load+ensure_ascii=False try/except/else/finally执行顺序- 常见异常:ValueError / ZeroDivisionError / IndexError / KeyError / TypeError / FileNotFoundError
raise、自定义异常、raise ... from ...- 综合:文件读取 + 异常容错