【python零基础教程第5讲】文件I/O操作与异常处理
Python文件I/O操作与异常处理:从入门到工程实践
前言
文件操作是程序与外部世界交互的基础,而异常处理则是保证程序健壮性的关键机制。无论是读取配置文件、写入日志,还是处理二进制数据,掌握Python的文件I/O和异常处理都是每个开发者必备的核心技能。本文将系统讲解Python中的文件读写、路径操作和异常处理,从基础用法到底层原理,帮助你编写出既功能完善又稳定可靠的代码。
一、文件读写基础
1.1open()函数:文件操作的入口
Python使用内置的open()函数打开文件,其完整语法如下:
open(file,mode='r',buffering=-1,encoding=None,errors=None,newline=None,closefd=True,opener=None)核心参数说明:
- file:文件路径(字符串或
Path对象),必需。 - mode:打开模式,决定读写权限和文件类型。
- encoding:文本模式下的字符编码,推荐显式指定
'utf-8'。
1.2 文件模式详解
open()支持多种模式,下表总结了最常用的模式:
| 模式 | 含义 | 文件存在性处理 | 指针位置 |
|---|---|---|---|
'r' | 只读文本(默认) | 必须存在 | 开头 |
'w' | 只写文本 | 不存在则创建,存在则清空 | 开头 |
'a' | 追加文本 | 不存在则创建,存在则保留 | 末尾 |
'rb' | 只读二进制 | 必须存在 | 开头 |
'wb' | 只写二进制 | 不存在则创建,存在则清空 | 开头 |
'r+' | 读写文本 | 必须存在 | 开头 |
'w+' | 读写文本 | 不存在则创建,存在则清空 | 开头 |
关键规则:文本模式(t)可省略,二进制模式(b)必须显式写出。
1.3 基础操作流程:打开 → 操作 → 关闭
文件操作必须遵循“打开-操作-关闭”的流程,否则可能导致资源泄露。强烈推荐使用with语句自动关闭文件,无需手动调用close()。
示例1:文本文件读写
# 写入文件withopen('test.txt','w',encoding='utf-8')asf:f.write('Hello, Python!\n')f.write('这是中文内容\n')# 读取文件withopen('test.txt','r',encoding='utf-8')asf:content=f.read()print('文件内容:\n',content)示例2:二进制文件操作(如图片复制)
withopen('source.jpg','rb')assrc,open('copy.jpg','wb')asdst:data=src.read()# 读取二进制数据dst.write(data)# 写入二进制数据print('图片复制完成')二进制文件(非文本文件)需用'rb'/'wb'模式,且无需指定encoding。
二、with上下文管理器:资源管理的利器
2.1 为什么需要with语句
传统文件操作的问题在于:如果忘记调用close(),或者操作过程中发生异常,文件可能无法正确关闭,导致资源泄漏。例如:
# 不推荐的做法f=open('data.txt','w')f.write('some data')# 如果这里发生异常,f.close()不会执行f.close()2.2with语句的优势
with语句(上下文管理器)能确保资源被正确释放,无论是否发生异常:
withopen('data.txt','w',encoding='utf-8')asf:f.write('some data')# 离开with块后,文件自动关闭2.3 上下文管理协议
with语句的背后是上下文管理协议,需要实现两个特殊方法:
__enter__():进入上下文时调用,返回资源对象。__exit__():退出上下文时调用,负责清理资源。它接收异常类型、值、回溯对象三参数,可决定是否“压制”异常(返回True)或继续传播(返回False或None)。
2.4 使用contextlib简化实现
对于自定义资源,可以使用contextlib.contextmanager装饰器简化上下文管理器的编写:
fromcontextlibimportcontextmanager@contextmanagerdefmanaged_file(filename,mode):f=open(filename,mode,encoding='utf-8')try:yieldffinally:f.close()withmanaged_file('test.txt','w')asf:f.write('Hello from contextmanager!')三、文件读取的多种方式
根据需求不同,文件读取有多种方式,灵活选择可提高效率(尤其处理大文件时):
| 方法 | 功能说明 | 适用场景 |
|---|---|---|
read() | 无参:读取全部内容;有参n:读取至多n字节 | 小文件;二进制大文件分块处理 |
readline() | 读取一行内容(含换行符) | 按行处理文件(如日志解析) |
readlines() | 读取所有行,返回列表 | 中等大小文件,需按行索引访问 |
for line in f: | 自动逐行读取,内存友好 | 大/超大文件,顺序处理行数据 |
示例:逐行读取大文件
withopen('large_log.txt','r',encoding='utf-8')asf:forlineinf:process(line)# 逐行处理,不占用大量内存四、路径操作:os模块基础与pathlib现代路径处理
4.1os.path模块(传统方式)
os.path提供了传统的路径操作函数:
importos# 路径拼接path=os.path.join('data','subdir','file.txt')# 获取文件名filename=os.path.basename(path)# 'file.txt'# 获取目录dirname=os.path.dirname(path)# 'data/subdir'# 获取扩展名ext=os.path.splitext(path)# '.txt'# 绝对路径abs_path=os.path.abspath(path)# 判断是否存在exists=os.path.exists(path)4.2pathlib模块(推荐方式)
Python 3.4+引入的pathlib提供了面向对象的路径操作,更加直观和现代:
frompathlibimportPath# 创建路径对象p=Path('data')/'subdir'/'file.txt'# 获取文件名print(p.name)# 'file.txt'# 获取目录print(p.parent)# 'data/subdir'# 获取扩展名print(p.suffix)# '.txt'# 绝对路径print(p.resolve())# 判断是否存在print(p.exists())# 读取文件内容(一行搞定)content=p.read_text(encoding='utf-8')# 写入文件p.write_text('Hello pathlib!',encoding='utf-8')4.3 对比总结
| 操作 | pathlib(推荐) | os.path(旧式) |
|---|---|---|
| 路径拼接 | p / 'file.txt' | os.path.join(p, 'file.txt') |
| 获取文件名 | p.name | os.path.basename(p) |
| 获取目录 | p.parent | os.path.dirname(p) |
| 获取扩展名 | p.suffix | os.path.splitext(p) |
| 绝对路径 | p.resolve() | os.path.abspath(p) |
| 是否存在 | p.exists() | os.path.exists(p) |
| 读取文件 | p.read_text() | open(p).read() |
最佳实践:在新项目中优先使用pathlib,它更简洁、更面向对象,且跨平台兼容性更好。
五、异常处理机制
5.1 异常类型概览
Python中常见的异常类型包括:
FileNotFoundError:文件不存在时抛出(继承自OSError)ValueError:传入无效参数时抛出IndexError:序列索引超出范围时抛出KeyError:字典键不存在时抛出TypeError:操作或函数应用于不适当类型的对象时抛出ZeroDivisionError:除零时抛出
5.2try-except-else-finally完整结构
Python的异常处理采用try-except-else-finally结构,各部分作用如下:
try:# 可能出错的代码result=risky_operation()exceptValueErrorase:# 捕获特定异常print(f"值错误:{e}")except(TypeError,IndexError)ase:# 捕获多个异常(元组形式)print(f"类型或索引错误:{e}")exceptExceptionase:# 捕获所有其他异常(谨慎使用)print(f"未知错误:{e}")else:# 没有异常时执行print(f"操作成功,结果为:{result}")finally:# 无论是否异常都执行(清理资源)print("清理工作完成")执行流程:
- 执行
try子句中的代码。 - 如果没有触发异常,跳过
except,执行else子句。 - 如果触发异常,跳过
try中剩余代码,匹配except子句并执行。 - 无论是否异常,最终都会执行
finally子句。
5.3 多异常分开捕获 vs 统一捕获
分开捕获(推荐):针对不同异常做不同处理
try:f=open('myfile.txt')s=f.readline()i=int(s.strip())exceptFileNotFoundError:print("文件不存在,请检查路径")exceptValueError:print("文件内容无法转换为整数")exceptOSErroraserr:print(f"操作系统错误:{err}")统一捕获:使用Exception作为通配符(但应谨慎,避免掩盖真实问题)
try:# 一些操作passexceptExceptionase:print(f"发生错误:{e}")# 记录日志后重新抛出,让上层处理raise最佳实践:尽可能捕获具体的异常类型,避免裸except:或过于宽泛的捕获。
5.4 主动抛出异常:raise
当检测到不符合预期的状态时,可以主动抛出异常:
defdivide(a,b):ifb==0:raiseValueError("除数不能为零")returna/btry:result=divide(10,0)exceptValueErrorase:print(f"计算错误:{e}")5.5 自定义异常
通过继承Exception类,可以创建业务语义化的自定义异常:
classInvalidAgeError(Exception):"""年龄无效时抛出的异常"""def__init__(self,age,message="年龄必须在0-150之间"):self.age=age self.message=messagesuper().__init__(self.message)defset_age(age):ifnot(0<=age<=150):raiseInvalidAgeError(age)print(f"年龄设置为:{age}")try:set_age(200)exceptInvalidAgeErrorase:print(f"错误:{e.message},你输入了:{e.age}")5.6 异常链与raise from
当在异常处理中抛出另一个异常时,可以使用raise from保留原始异常信息:
try:int('abc')exceptValueErrorase:raiseRuntimeError("转换失败")frome六、实战项目:健壮的日志记录系统
结合文件I/O和异常处理,实现一个简单的日志记录器:
frompathlibimportPathfromdatetimeimportdatetimeclassLogger:def__init__(self,log_dir="logs"):self.log_dir=Path(log_dir)self.log_dir.mkdir(exist_ok=True)# 自动创建目录deflog(self,level,message):timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")log_entry=f"[{timestamp}] [{level}]{message}\n"log_file=self.log_dir/f"{datetime.now().strftime('%Y-%m-%d')}.log"try:withopen(log_file,'a',encoding='utf-8')asf:f.write(log_entry)exceptPermissionError:print(f"错误:没有权限写入日志文件{log_file}")exceptOSErrorase:print(f"文件操作错误:{e}")else:print(f"日志已记录:{log_entry.strip()}")defread_logs(self,date_str=None):ifdate_strisNone:date_str=datetime.now().strftime("%Y-%m-%d")log_file=self.log_dir/f"{date_str}.log"try:returnlog_file.read_text(encoding='utf-8')exceptFileNotFoundError:returnf"没有找到{date_str}的日志文件"exceptExceptionase:returnf"读取日志时发生错误:{e}"# 使用示例logger=Logger()logger.log("INFO","系统启动")logger.log("ERROR","数据库连接失败")print(logger.read_logs())七、常见陷阱与最佳实践
7.1 陷阱1:忘记指定编码
不指定encoding时,Python会使用系统默认编码(Windows上可能是gbk),导致跨平台时出现乱码。始终显式指定encoding='utf-8'。
7.2 陷阱2:路径分隔符硬编码
在Windows上使用\作为路径分隔符会导致跨平台问题。使用pathlib或os.path.join自动处理分隔符。
7.3 陷阱3:裸except捕获
try:# 代码except:# 不推荐!会捕获SystemExit、KeyboardInterrupt等pass应使用except Exception:来捕获常规异常,或捕获具体类型。
7.4 陷阱4:文件操作中的竞态条件
在检查文件存在后立即操作,可能被其他进程修改。使用异常处理代替先检查后操作:
# 不推荐ifos.path.exists('data.txt'):withopen('data.txt')asf:...# 推荐try:withopen('data.txt')asf:...exceptFileNotFoundError:print("文件不存在")7.5 最佳实践总结
| 场景 | 推荐做法 |
|---|---|
| 文件读写 | 始终使用with语句 |
| 路径操作 | 使用pathlib替代os.path |
| 编码处理 | 始终显式指定encoding='utf-8' |
| 异常处理 | 捕获具体异常,避免裸except |
| 大文件处理 | 使用迭代器逐行读取 |
| 配置文件 | 使用JSON/YAML等标准格式 |
八、底层原理:文件描述符与异常继承体系
8.1 文件描述符
操作系统通过文件描述符(File Descriptor)管理打开的文件。为什么必须关闭文件?
- 释放文件描述符(系统资源有限)
- 确保缓冲区数据写入磁盘
- 允许其他进程访问文件
8.2 异常继承体系
Python异常类的层次结构:
BaseException ├── SystemExit ├── KeyboardInterrupt ├── GeneratorExit └── Exception ├── StopIteration ├── ArithmeticError (ZeroDivisionError, ...) ├── LookupError (IndexError, KeyError) ├── OSError (FileNotFoundError, PermissionError, ...) └── ValueError结语
文件I/O和异常处理是Python编程的两大基石。通过本文的学习,你应该掌握了:
- 使用
open()和with语句安全读写文件 - 二进制文件的读写操作
- 使用
pathlib进行现代路径管理 - 完整的异常处理结构(try-except-else-finally)
- 多异常捕获、主动抛出和自定义异常
记住:显式优于隐式、简单优于复杂、错误不应被静默忽略。将这些原则应用到日常开发中,你的代码将更加健壮和可维护。