第26天,Python学习进度条终于走到文件读写和异常处理这个坎儿上。这俩东西在教程里往往被放在不起眼的角落,但实际写起脚本、处理数据、做小工具,几乎每一个真实程序都逃不开它们。我自己的感受是,前面函数、列表、字典如果说是搭骨架,那文件读写就是让程序真正“活”起来的第一步——程序跑完能把结果留下来,能读取外部输入,才不是自娱自乐。而异常处理,则是从“写着玩”迈向“能用”的一道分水岭。
这篇记录我按自己第26天的实际学习思路来梳理:前半场主攻文件读写的各种细节和坑,后半场把异常处理的完整套路捋清楚,最后用一个小案例把两者串起来。适合正在学Python、刚过完基础语法想往实战走的朋友,也适合复习时发现自己对文件操作一知半解的初学者。今天这篇里讲的踩坑记录,都是我自己写代码试出来的,不是背文档。
1. 第26天该学什么:先把学习路数理清楚
1.1 一个朴素但有效的打卡路线
第26天这个位置,说尴尬也尴尬。前25天如果按常见学习路径走,已经覆盖了变量、数据类型、流程控制、函数、列表推导式、模块导入这些基础内容。但这个阶段的代码基本是“一次性”的——跑完就没了,数据只存在内存里。想做个像样的东西出来,第一步就是要让程序能和外部世界打交道,说的直白一点:读写文件。
我给自己定的这阶段路线很直白:
- 文本文件的读和写,搞懂open、close、with
- 弄清楚编码为什么会导致乱码
- 用json搞定结构化数据的存储和加载
- 把异常处理try、except、finally用熟练
- 写一个小工具,把文件读写和异常处理结合起来
这五步走完,你已经具备写“有实际价值”的程序的基本能力了。我见过很多初学者卡在“学完语法但做不出东西”这个阶段,很大程度上就是因为文件读写没学好——想写个词频统计、日志分析都动不了手,自然觉得编程没意思。
1.2 为什么偏偏是文件读写和异常处理
我自己的体会是,这两块内容属于典型的“用着不难,但坑特别多”的部分。你看PDF文件,Read、Write、Append、二进制模式,模式参数就那么几个,背下来很容易。但真正上手才会撞到编码问题、路径分隔符问题、文件不存在问题、读写速度问题。如果只背API不看细节,写出来的代码要么跑不起来,要么跑起来以后莫名其妙报错。
异常处理就更不用说了。很多初学者觉得 try-except 就是“把有可能出错的代码包起来”,这个理解没错,但如果只学这一层,写出来的代码往往把异常全吞掉,出了问题连日志都没有。第26天正是建立“程序凒壮性”意识的起点——不是不报错,而是报错的时候你能知道错在哪、程序不死掉、用户看得懂提示。
所以今天这篇不是把官方文档翻译一遍,而是把我实操过程中真正有用的细节、真正踩过的坑,按场景重新组织一遍。
2. 文件读写核心:open函数到底怎么用才靠谱
2.1 模式参数别死记硬背,用场景推
Python里打开文件的入口就是 open(),最常见的用法长这样:
file_obj = open("data.txt", "r", encoding="utf-8") content_text = file_obj.read() file_obj.close()第一个参数是文件路径,第二个参数是模式,第三个参数是指定编码。模式参数是最容易搞混的,我看到很多人靠背表格:r是读、w是写、a是追加、rb是二进制读……背是背下来了,但一换场景就懵。
这里我推荐一个更不容易出错的理解方式——不要按字母去背,按你要干什么去推:
- 你要读内容,就用 "r",这是最安全的选择,文件不存在会报错,不会误删数据
- 你要从零开始写新内容,就用 "w",它会直接覆盖原文件,文件不存在会新建
- 你要在文件末尾追加内容,就用 "a",原有内容保留,新内容接在后面
- 你要处理图片、音频、压缩包,这些非文本的二进制文件,才用 "b" 系列: "rb"、"wb"、"ab"
"r+"、"w+"、"a+" 这类的带上加号的模式,是同时允许读写。我实际用下来的建议是:新手阶段尽量不要碰带+的模式,逻辑很容易绕晕。比如 "w+" 会先清空文件再让你读,很多人忘记这一点,以为可以边读边改,结果一打开内容全没了。
一个小事:默认模式是 "r",也就是你不写第二个参数,程序会按只读模式打开,文件不存在时直接抛 FileNotFoundError。所以宁可写全,不要依赖默认值。
2.2 编码问题:乱码背后的真正原因
编码问题可能是文件读写里最影响体验的坑,没有之一。很多人第一次跑 open("test.txt", "r") 读出来一堆乱码,第一反应是文件坏了,其实大概率是编码对不上。
Python 3 里 open() 如果不指定 encoding 参数,会使用平台默认编码。我自己之前在 Windows 上跑就遇到过,系统默认编码经常是 GBK,而文件是 UTF-8 编码保存的,两边对不上,读出来就是乱码。解决方式很简单,打开文件时显式指定编码:
content_text = open("来源文件.txt", "r", encoding="utf-8").read()反过来也有一种情况,文件本身是 GBK 编码,你强制用 utf-8 去读,也会乱码甚至直接报 UnicodeDecodeError。这时候把 encoding 换成 "gbk" 就好。这个“双方对齐”的思路比死记什么编码表重要得多。
写入的时候同样要显式指定编码。我之前写配置文件时吃过亏:只写了 open("out.txt", "w"),结果文件写出来是 GBK 编码,拿到别的环境一读就乱。后来统一养成习惯,读写都写全 encoding="utf-8"。
提示:写文本文件时如果看到 UnicodeEncodeError,十有八九是编码问题。不要尝试用什么“忽略错误”的参数硬编码去糊弄,先排查是不是编码没对齐。
2.3 路径问题:反斜杠、正斜杠和相对路径
路径这一块,Windows 用户踩的坑最多。Windows 的路径分隔符是反斜杠 \,但在 Python 字符串里反斜杠是转义符,所以如果你写:
file_obj = open("C:\Users\test\data.txt")Python 会把 \U、\t 这些当成转义字符处理,轻则路径错误,重则直接报错。有三种常用解法:
- 写成双反斜杠:"C:\Users\test\data.txt"
- 写成原始字符串:r"C:\Users\test\data.txt"
- 统一用正斜杠:"C:/Users/test/data.txt"(Windows 也认正斜杠)
我个人的习惯是尽量用相对路径,也就是相对于当前工作目录的路径。比如脚本和文件在同一目录下,直接写文件名就行。这样代码拿到任何机器上都不会因为盘符不同而挂掉。
3. with open:上下文管理器的正确姿势
3.1 为什么不能只靠 close()
相信你注意到了,我在上面第一段代码里手写了 close()。这么做的问题在于:如果程序在 read() 或 write() 中间抛了异常,close() 那一行根本没机会执行,文件句柄就泄漏了。文件句柄开着不放,最直接的表现是文件被占用——在 Windows 上你连删都删不掉,在 Linux 上也可能导致资源耗尽。
正确的做法是用 with 语句:
with open("data.txt", "r", encoding="utf-8") as file_obj: content_text = file_obj.read()with 语句会在代码块执行完毕后自动关闭文件,不管中间有没有异常。这是 Python 里最典型的“上下文管理器”用法。我自己的习惯是:凡是 open() 打开文件,一律用 with,除非有特别极端的场景。这个习惯形成之后,我几乎没有再遇到文件句柄被占用的问题。
3.2 读文件的几种姿势
读文件也不是只有 read() 一种选择。根据文件大小和用途,至少要掌握三种方式:
# 一次性读入全部内容,小文件适合 content_text = file_obj.read() # 按行读取,得到一个列表,每行是一个元素 lines_list = file_obj.readlines() # 迭代读取,逐行处理,大文件首选 for one_line in file_obj: print(one_line)第三种逐行迭代的方式最值得养成习惯,因为文件特别大的时候,read() 会把所有内容装进内存,非常容易撑爆内存。我记得有次处理一个几个 GB 的日志文件,readlines() 直接内存吃满,换成 for 循环逐行读,内存占用瞬间降到几十 MB。就这个体验,值得点个赞。
3.3 写文件时注意刷新和缓冲
写文件的时候有一个细节很多人忽视:写入不一定立即落盘。Python 的写入操作会先写到内存缓冲区,缓冲区满了或者文件关闭时才真正写到磁盘。如果程序在中途崩溃,会发现文件存在但内容是空的或者不全。
所以如果你有“边写边保存”的需求,可以考虑在关键节点调用 flush(),把缓冲区内容强制写入磁盘。但也不要过度使用,频繁写盘会影响性能。折中的办法是:程序结束前写完整逻辑,with 会自动帮你做收尾。如果程序需要长期运行,比如写日志,那么在每次写完后 flush 是比较稳妥的。
4. 用json搞定结构化数据存取
4.1 dump和dumps到底选哪个
学会了文本文件的读写,下一个自然的需求就是存结构化数据。比如你有一个字典,里面有学生名字、成绩、列表,直接存成字符串再读回来就乱七八糟了。这时候就该 json 模块登场。
json 模块里有两对函数特别容易混淆:
import json data_dict = {"name": "小明", "scores": [90, 88, 100]} # dumps:把Python对象转成字符串 json_str = json.dumps(data_dict, ensure_ascii=False) print(json_str) # dump:把Python对象转成字符串,并直接写入文件 with open("data.json", "w", encoding="utf-8") as file_obj: json.dump(data_dict, file_obj, ensure_ascii=False) # 反向操作 # loads:把字符串转回Python对象 restore_data = json.loads(json_str) # load:从文件读内容并转回Python对象 with open("data.json", "r", encoding="utf-8") as file_obj: load_data = json.load(file_obj)记忆方法很简单:带 s 的是 String(字符串),不带 s 的是跟文件打交道。dumps 和 loads 是“内存到内存”,dump 和 load 是“内存到文件”。
4.2 关于 ensure_ascii 的坑
上面代码里的 ensure_ascii=False 值得单独讲一下。json 模块默认会把非 ASCII 字符转成 \uXXXX 这种转义形式,比如“小明”会变成 "\u5c0f\u660e"。这么做的好处是兼容性极强,坏处是你打开文件看的时候满屏转义符,并且文件体积变大。
如果你存的是中文内容,还是希望人能直接读,那就在写入的时候加上 ensure_ascii=False。这个参数不写的话中文全变转义符,读的时候还能还原,但看着实在痛苦。
load 和 loads 也有一个细节需要留心:当文件内容为空时,json.load 会直接抛异常。如果文件可能被清空或者缺失,建议先做判断,再加载,或者用异常处理兜底。这也是完整的工程思维的一部分。
4.3 配置文件用什么格式:json、yaml还是ini
这里分享一点做小工具时的选型经验。如果是自用的小脚本,我强烈推荐 json 做配置格式:Python 原生支持,读写代码量最小,存储结构清晰,稍大一点也足够用。YAML 可读性更好、支持注释,但需要安装第三方库,处理不当时还有解析歧义。ini 文件虽然有 configparser 但表达能力有限,嵌套结构没办法优雅表达。
所以 80% 的自用小工具场景,json 是最省心的选择。如果项目需要给外部用户提供配置能力、需要写注释,再考虑 YAML 也不迟。
5. 异常处理的完整套路
5.1 先理解什么情况下才会触发异常
异常不是“玄学”,它是程序运行时遇到错误时抛出的信号。常见的异常类型包括:文件不存在时抛 FileNotFoundError,访问列表越界抛 IndexError,字典里键不存在抛 KeyError,类型和值不匹配抛 TypeError 和 ValueError。这些都是 Python 内置的异常类,互相之间存在继承关系。
新手最容易犯的错是用一大段 try 把所有代码包住,然后 except 后面不写任何类型:
try: with open("data.txt", "r") as file_obj: content_text = file_obj.read() number_value = int(content_text) except: print("出错了")这问题在哪?第一,它会捕获所有异常,包括你根本没预料的错误,结果错误信息被吞掉,你连调试头绪都没有。第二,它不会告诉你具体错在哪个环节,文件读失败和转换失败混在一起,排查起来效率极低。
5.2 针对性的捕获,分类型处理
正确的做法是分类型捕获:
try: with open("data.txt", "r", encoding="utf-8") as file_obj: content_text = file_obj.read() number_value = int(content_text.strip()) except FileNotFoundError: print("文件不存在,请检查路径") except ValueError: print("内容不是有效的数字")这样每个错误都有对应的处理逻辑。让人一眼看出哪里可能会出错、出错以后的行为是什么。在工程上这叫“针对性异常处理”,也是别人读你代码时最容易感受到专业度的地方。
5.3 用 else 和 finally 理顺逻辑
try-except 还可以搭配 else 和 finally,它们的作用经常被网上的教程省略。我自己用下来感觉非常有用:
try: with open("data.json", "r", encoding="utf-8") as file_obj: config_data = json.load(file_obj) except FileNotFoundError: print("配置文件缺失,使用默认配置") config_data = {} except json.JSONDecodeError: print("配置文件损坏,使用默认配置") config_data = {} else: print("配置文件加载成功") finally: print("配置加载流程执行完毕")else 块里的代码只在 try 块没有抛出异常时执行。可以把“成功后的额外操作”放进去,这样不会把成功逻辑和可能出错的逻辑混在一起。finally 块则无论有没有异常都会执行,适合放清理操作,比如关闭连接、释放锁。
这里有一个我踩过多次的坑:如果 try 里已经用 with 处理了文件,finally 里不需要再 close(),多此一举。但如果用的是更底层的资源(比如 socket、临时目录),在 finally 里做清理是标准实践。
5.4 自定义异常和主动抛异常
到了第26天,可以开始接触一个更进阶的内容:自定义异常。虽然它不像前面那些基础一样每天必须用,但理解它能让你的程序结构上一个台阶。
使用方法很简单,继承内置的 Exception 类就行:
class ConfigValidateError(Exception): pass def validate_config(config_data_dict): if "timeout" not in config_data_dict: raise ConfigValidateError("配置缺少timeout字段")自定义异常最大的好处,是把“业务层的错误”和“语法层的错误”区分开。别人读到你的代码时,看到 raise ConfigValidateError,就能立刻理解这是业务流程里的校验失败,而不是网络问题、文件问题。这种“语义化报错”的意识,越早建立越好。
6. 实战小项目:写一个带容错的配置读取工具
6.1 一个真实高频的需求
文件读写和异常处理最典型的结合场景,就是读取配置文件。我几乎每次写自用小工具都会重复这个需求:程序启动时读一个配置文件,里面存放可调整的参数,比如超时时间、需要扫描的目录、输出格式等。如果配置文件不存在或者格式坏了,程序不应该崩溃,而是用默认值继续运行,并给出提示。
这里我直接给出一个可落地的完整示例,核心是把 JSON 配置读取做成了一个独立的函数:
import json def load_config(file_path, default_config): try: with open(file_path, "r", encoding="utf-8") as file_obj: config_data = json.load(file_obj) if not isinstance(config_data, dict): raise ValueError("配置格式应为JSON对象") except FileNotFoundError: print(f"警告:配置文件 {file_path} 不存在,使用默认配置") return default_config except json.JSONDecodeError: print(f"警告:配置文件 {file_path} 解析失败,使用默认配置") return default_config except ValueError as error: print(f"警告:配置文件内容异常:{error},使用默认配置") return default_config else: # 把默认配置和用户配置合并,用户配置优先 merge_config = dict(default_config) merge_config.update(config_data) return merge_config default_settings = {"timeout": 5, "output_dir": "./out", "verbose": False} config = load_config("settings.json", default_settings) print("当前配置:", config)这个例子虽然短,但已经把今天讲的核心点全部用上了:
- 用 with 管理文件句柄,不会泄漏
- 显式指定 utf-8 编码,避免平台差异
- 三个不同的异常类型分开处理,不复用一个大 try 块
- else 分支放成功逻辑,不会被误报异常污染
- 默认配置合并机制,让程序在文件缺失时依然能正常运行
直接复制到你的项目里,改一改默认配置就能用。
6.2 还可以怎么扩展
如果想把这段代码再往前推一步,可以考虑几个方向:
一是支持自动创建配置文件。检测到文件不存在时,把默认配置直接写入文件,方便用户手动修改,下次启动时就能读到有效配置。二是增加配置的单元测试。把异常情况(文件不存在、文件内容非法、字段缺失)分别写进测试用例,跑一遍确认程序行为符合预期。三是配合今天的异常处理知识,把 load_config 抛出的业务异常定义成自定义异常类型,让上层调用时能精确捕获。
我自己的实践体会是,小工具的价值不在于功能多复杂,而在于它的失败模式是可预测的。用户少了一个配置文件、改错一个字段类型,程序都不会崩溃成一堆红字,这就是今天这些基础知识带来的回报。
7. 常见问题与排查技巧实录
7.1 高频问题速查表
把这段时间积累的高频问题整理成一个表格,遇到问题对着查:
| 问题现象 | 可能原因 | 建议解法 |
|---|---|---|
| 读文本出现乱码 | 读写编码不一致 | 读写时都显式指定 encoding="utf-8",或改为与文件相同的编码 |
| 文件打不开,报 FileNotFoundError | 路径错误或文件不存在 | 用相对路径,确认脚本工作目录,打印当前目录定位 |
| 写文件后内容丢失 | 写模式用了 "w" 被覆盖,或没有 close/flush | 追加用 "a" 模式,重要文件先备份,用 with 自动关闭 |
| 把中文字符写入 json 变成 \uXXXX | 默认 ensure_ascii=True | 写入时加 ensure_ascii=False |
| 用 r+ 打开文件读取为空 | 忽略了 w+ / r+ 的截断行为 | 新手先不用带+的模式,读和写分开处理 |
| 异常被吞没有日志 | except 里只写了 pass | 至少打印 error 信息,或写入日志文件 |
| 程序报 UnicodeDecodeError | 文件编码与指定编码不匹配 | 用二进制模式读取,或尝试其他编码测量工具 |
这些问题的共同点在于:不是语法不会,而是没有建立起“文件状态”的意识。文件打开时的状态是只读、覆盖还是追加,写入缓冲区是否刷新,编码是否对齐,每一条都对应着程序运行时的一个状态。
7.2 三个让效率翻倍的排查技巧
第一个技巧:在报错信息里看上下文。Python 的 traceback 是从下往上读的,最后一行是错误类型和错误信息,往上翻是关键代码行。很多人只看最后一行的“FileNotFoundError”就慌了,其实往上两行就能看到到底是哪个文件路径出错了。
第二个技巧:打印当前工作目录。这招在排查“文件找不到”时特别有用。用 os.getcwd() 打印当前目录,确认脚本觉得“当前在哪里”,很多路径困惑瞬间解开。
import os print(os.getcwd())第三个技巧:用二进制模式确认文件内容。如果文件读出来总是乱码,先别急着改代码,用编辑器或者命令行以二进制方式打开文件看原始字节序列,能非常直观地判断文件的真实编码。顺便说一句,很多编码问题的本质是“用错误的方式解释字节”,确认原始字节比瞎猜编码快得多。
7.3 这些坑一定要亲身踩一遍
最后说几句可能不太中听、但非常真实的建议:光看别人的踩坑记录没用,一定要亲手把这些问题触发一遍。我自己的学习路径是,看完教程后专门写了一个故意出错的脚本:故意不 close 文件、故意不指定编码读 GBK 文件、故意用 w 模式覆盖重要文件。在安全的环境下把这些错误穷举一遍,眼见报错信息长什么样,之后真正在工作中遇到就不慌了。
这个过程大概会花掉一两个小时,但换来的收益很大。学编程到第26天,早就过了“看视频点头”的舒适区,亲手碰一遍错误、亲手解决一个 bug,比刷十节网课都管用。这种“主动试错”的学习方式,也是我到了这个阶段觉得自己写代码明显更稳的重要原因。