【python零基础教程第5讲】文件I/O操作与异常处理

📅 2026/7/7 17:05:43 👁️ 阅读次数 📝 编程学习
【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)或继续传播(返回FalseNone)。

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.nameos.path.basename(p)
获取目录p.parentos.path.dirname(p)
获取扩展名p.suffixos.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("清理工作完成")

执行流程

  1. 执行try子句中的代码。
  2. 如果没有触发异常,跳过except,执行else子句。
  3. 如果触发异常,跳过try中剩余代码,匹配except子句并执行。
  4. 无论是否异常,最终都会执行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上使用\作为路径分隔符会导致跨平台问题。使用pathlibos.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)管理打开的文件。为什么必须关闭文件?

  1. 释放文件描述符(系统资源有限)
  2. 确保缓冲区数据写入磁盘
  3. 允许其他进程访问文件

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)
  • 多异常捕获、主动抛出和自定义异常

记住:显式优于隐式、简单优于复杂、错误不应被静默忽略。将这些原则应用到日常开发中,你的代码将更加健壮和可维护。