Python 文件操作、异常处理与生成器深入解析
【一】控制文件内指针移动
【1】前言
withopen("1.text",'r',encoding="utf-8")asfp:data=fp.read().split(" ")data.insert(1,"dear")data=' '.join(data)print(data)withopen("2.text",'w',encoding="utf-8")asfp:fp.write(data)【2】read的参数
withopen("2.text",'r',encoding="utf-8")asfp:# fp.read(数字)# read 里面的数字可以控制当前 读取的内容的字符个数 并且 读是从头开始读的# read(数字) 数字默认是 -1 从头读到尾data=fp.read(-1)print(data)【3】seek移动指针
withopen("2.text",'r+',encoding="utf-8")asfp:# tell 查看当前指针所在的字节位置# print(fp.tell()) # 0# fp.write('Hello 蚩梦')# data = fp.read(-1)# print(data) # Hello dear 蚩梦# 一个中文占 3 个字节# print(fp.tell()) # 17# seek 方法可以控制指针移动# offset 移动几个字节# whence 移动模式# 0 : 默认的模式,按照文件开头作为参照向后移动指定字节数# 1 : 按照当前所在位置作为参照向后移动指定字节数# 2 : 按照文件末尾作为参照向后移动指定字节数# print(fp.tell())# # fp.seek(4, 0)# print(fp.tell())# 1 只能用在二进制数据上 ,文本数据不支持 相对移动# fp.seek(2, 1)# fp.seek(0, 2)# print(fp.tell()) # 17# fp.write("11111")# 控制光标移动到指定字节位置的时候写入数据是从当前字节位置覆盖写入内容# 一个中文占3个字节,如果写入的时候恰巧是中文并且写入的不够三个字节就会导致文件出现乱码fp.seek(6,0)# print(fp.read(-1))# Hello 蚩梦# dear --=> 文件变成乱码fp.write("dea")【4】小结
# seek# seek(字节数,模式)# 0 : 默认的模式,按照文件开头作为参照向后移动指定字节数# 1 : 按照当前所在位置作为参照向后移动指定字节数(不能用于文本数据而是要用于二进制数据)# 2 : 按照文件末尾作为参照向后移动指定字节数# 控制光标移动到指定字节位置的时候 如果遇到中文一定要留意是 3个字节一个中文# 控制光标移动到指定字节位置的时候写入数据是从当前字节位置覆盖写入内容# a 模式的原理就是先将 光标移动到最后的位置 然后继续向下写内容# w 模式 用seek 移动到结尾和上面的a模式是一样的效果''' with open("自愿上班.jpeg", 'rb') as fp: fp.seek(5, 0) print(fp.tell()) fp.seek(5, 1) print(fp.tell()) '''【二】异常捕获
# 【一】什么是异常# 程序在运行过程中遇到的报错,导致程序崩溃的代码就是异常# 我们在程序运行过程中 不应该让程序报错 或者 在程序报错之后应该立马通知开发者# 并且有相应的异常重启机制# 【二】异常捕获# 内建异常:Python解释器自带的异常捕获机制# ValueError: invalid literal for int() with base 10: 'a'# 自定义异常:我们自己主动抛出的异常""" print(1) print(2) raise ValueError("不能打印3") print(4) """# 【三】内建异常# BaseException: 所有异常的基类# ● SystemExit:解释器请求退出# ● KeyboardInterrupt:用户中断执行(通常是输入^C)# ● Exception:常规错误的基类# ● StopIteration:迭代器没有更多的值# ● GeneratorExit:生成器(generator)发生异常来通知退出# ● StandardError:所有的内建标准异常的基类# ● ArithmeticError:所有数值计算错误的基类# ● FloatingPointError:浮点计算错误# ● OverflowError:数值运算超出最大限制# ● ZeroDivisionError:除(或取模)零 (所有数据类型)# ● AssertionError:断言语句失败# ● AttributeError:对象没有这个属性# ● EOFError:没有内建输入,到达EOF 标记# ● EnvironmentError:操作系统错误的基类# ● IOError:输入/输出操作失败# ● OSError:操作系统错误# ● WindowsError:系统调用失败# ● ImportError:导入模块/对象失败# ● LookupError:无效数据查询的基类# ● IndexError:序列中没有此索引(index)# ● KeyError:映射中没有这个键# ● MemoryError:内存溢出错误(对于Python 解释器不是致命的)# ● NameError:未声明/初始化对象 (没有属性)# ● UnboundLocalError:访问未初始化的本地变量# ● ReferenceError:弱引用(Weak reference)试图访问已经垃圾回收了的对象# ● RuntimeError:一般的运行时错误# ● NotImplementedError:尚未实现的方法# ● SyntaxError:Python 语法错误# ● IndentationError:缩进错误# ● TabError:Tab 和空格混用# ● SystemError:一般的解释器系统错误# ● TypeError:对类型无效的操作# ● ValueError:传入无效的参数# ● UnicodeError:Unicode 相关的错误# ● UnicodeDecodeError:Unicode 解码时的错误# ● UnicodeEncodeError:Unicode 编码时错误# ● UnicodeTranslateError:Unicode 转换时错误# print(1 / 0) # ZeroDivisionError: division by zero# 【三】警告异常# Warning(警告的基类)# ● DeprecationWarning:关于被弃用的特征的警告# ● FutureWarning:关于构造将来语义会有改变的警告# ● OverflowWarning:旧的关于自动提升为长整型(long)的警告# ● PendingDeprecationWarning:关于特性将会被废弃的警告# ● RuntimeWarning:可疑的运行时行为(runtime behavior)的警告# ● SyntaxWarning:可疑的语法的警告# ● UserWarning:用户代码生成的警告# 【四】如何主动抛出异常# 借助关键字 raise# 语法 raise [Exception [, args [, traceback]]]''' for i in range(10): if i == 2: raise ValueError("参数不能为2") '''# 【五】主动捕获异常# 【1】不带异常信息''' try: # 正常执行可能会发生错误的代码 int("a") except: # 异常处理机制 print(f"发生异常了") ... '''# 【2】只有单一的异常信息''' try: # 正常执行可能会发生错误的代码 # int("a") print(1 / 0) # 放指定的异常类型 当上面的代码遇到指定的错误的时候才会被捕获到 except ValueError: # 异常处理机制 print(f"发生异常了") ... '''# 【3】捕获多个异常信息''' try: # 正常执行可能会发生错误的代码 print(1 / 0) int("a") # 放了多个不同的异常类型 并且将异常信息赋给 变量 e except (ValueError, ZeroDivisionError) as e: # 在处理异常的时候可以通过变量e查看到当前的异常类型和异常信息 # 异常处理机制 # 发生异常了 invalid literal for int() with base 10: 'a' # 发生异常了 division by zero print(f"发生异常了 {e}") ... '''# 【4】捕获所有异常信息''' try: # 正常执行可能会发生错误的代码 # print(1 / 0) int("a") # print(1) # Exception 捕获所有异常类型 except Exception as e: # 在处理异常的时候可以通过变量e查看到当前的异常类型和异常信息 # 异常处理机制 print(f"发生异常了 {e}") ... # 当代码没有抛出异常时 , 结束正常代码会自动进入到 else 语句中 else: print(f"当前走入到 else 中") # 不管代码抛出还是不抛出异常都会走入到 finally 当中 finally: print(f"当前走入到 finally 中") '''# 【assert关键】# 断言# 当在程序中遇到某个条件为真的时候会抛出异常# 在某个功能里面必须传入某个参数的时候 如果没有传入就可以抛出 assert 异常''' for i in range(10): # if i == 2: # raise ValueError("不能为2") # assert 为真的判断条件,"异常信息" # 一般用在源码中 assert i == 2, "不能为2" '''【三】生成式
# 【一】列表推导式# 语法 [表达式 for 变量名 in 可迭代类型 if 条件]# 为了简洁代码# 【1】构建一个列表''' num_list = [] for i in range(10): num_list.append(i) print(num_list) '''''' num_list = [i for i in range(10)] print(num_list) '''# 【2】生成一个 从 0 -10 的数字的平方的列表''' num_list = [i ** 2 for i in range(10)] print(num_list) '''''' num_list = [i.upper() for i in "dream"] print(num_list) '''# 【3】嵌套循环num_one=[1,2,3]num_two=[4,5,6]''' res_list = [] for i in num_one: for j in num_two: res = i * j res_list.append(res) print(res_list) '''''' res_list = [i * j for i in num_one for j in num_two] print(res_list) '''''' res_list = [num_one[i] * num_two[i] for i in range(len(num_one))] print(res_list) '''# 【4】行列转换''' res = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]] # len(res) : 3 num_list = [] # [1, 2, 3, 4] for i in range(len(res[0])): num_list_inner = [] for line in res: num_list_inner.append(line[i]) num_list.append(num_list_inner) print(num_list) print([[line[i] for line in res] for i in range(len(res[0]))]) '''# 【5】加入判断条件''' num_list = [1, 2, 3, 4, 5] num_list_new = [] for i in num_list: if i % 2 == 0: num_list_new.append(i) print(num_list_new) print([i for i in num_list if i % 2 == 0]) '''# 【二】字典生成式# 语法 {变量1:变量2 for 变量1 in 可迭代对象: for 变量2 in 可迭代对象 if 表达式}my_tuple_list=[('name','橡皮擦'),('age',18),('class','no1'),('like','python')]data_dict={}forlineinmy_tuple_list:# line = ('name', '橡皮擦')key,value=line data_dict[key]=valueprint(data_dict)print({key:valueforkey,valueinmy_tuple_list})# 【三】元组生成式num=(iforiinrange(10)ifi%2==0)print([iforiinrange(10)ifi%2==0])# 生成器对象print(list((iforiinrange(10)ifi%2==0)))# <generator object <genexpr> at 0x106c3b4c0>【四】文件指针和异常处理进阶
【1】文本模式与二进制模式的区别
文本模式下,tell()和seek()的位置以文件流位置为准,编码后的字节数可能与字符串字符数不同。二进制模式下读写的对象是bytes,移动指针时可以精确按照字节处理。
text="你好,Python"withopen("demo.txt","w",encoding="utf-8")asfp:fp.write(text)withopen("demo.txt","rb")asfp:print(fp.read(3))# 中文 UTF-8 通常占 3 个字节print(fp.tell())# 3fp.seek(0)content=fp.read()print(content.decode("utf-8"))不要随意把文本文件的指针移动到一个中文字符的中间位置后再解码,否则可能出现UnicodeDecodeError。需要修改文本内容时,优先采用“读取、处理、整体写回”的方式,而不是直接覆盖中间字节。
【2】安全修改文本文件
frompathlibimportPath file_path=Path("message.txt")file_path.write_text("hello python",encoding="utf-8")content=file_path.read_text(encoding="utf-8")content=content.replace("python","world")file_path.write_text(content,encoding="utf-8")print(file_path.read_text(encoding="utf-8"))# hello world如果文件较大,可以使用临时文件逐行处理,再用新文件替换旧文件,避免一次性把全部内容加载到内存中。
【3】文件模式组合
# rt:文本读取模式,t 是默认模式# rb:二进制读取模式# wt:文本覆盖写模式# wb:二进制覆盖写模式# at:文本追加模式# ab:二进制追加模式withopen("data.bin","wb")asfp:fp.write(bytes([65,66,67]))withopen("data.bin","rb")asfp:print(fp.read())# b'ABC'【4】文件异常处理
try:withopen("config.txt","r",encoding="utf-8")asfp:config=fp.read()exceptFileNotFoundError:print("配置文件不存在")exceptPermissionError:print("没有权限读取配置文件")exceptUnicodeDecodeError:print("配置文件编码不正确")else:print("配置读取成功")finally:print("文件读取流程结束")异常捕获要尽量具体。直接使用裸except:会把KeyboardInterrupt等非普通业务异常也捕获,不利于调试;通常使用except Exception as error:捕获常规运行时异常即可。
【五】异常处理最佳实践
【1】异常的执行流程
try:number=int(input("请输入整数:"))result=100/numberexceptValueError:print("输入内容不是整数")exceptZeroDivisionError:print("除数不能为 0")else:print(f"计算结果:{result}")finally:print("本次计算结束")try中放可能出错的代码,except处理异常,else在没有异常时执行,finally无论是否出错都会执行。不要把大量无关代码全部放进try,否则很难定位真正的错误来源。
【2】主动抛出异常
defset_age(age):ifnotisinstance(age,int):raiseTypeError("年龄必须是整数")ifnot0<=age<=120:raiseValueError("年龄必须在 0 到 120 之间")returnagetry:print(set_age(18))# print(set_age("18"))# print(set_age(180))except(TypeError,ValueError)aserror:print(f"参数错误:{error}")【3】自定义异常
classInsufficientBalanceError(Exception):"""余额不足时抛出的业务异常。"""defwithdraw(balance,amount):ifamount>balance:raiseInsufficientBalanceError("余额不足,无法完成取款")returnbalance-amounttry:balance=withdraw(100,200)exceptInsufficientBalanceErroraserror:print(error)自定义异常适合表达明确的业务错误。异常信息要能够帮助定位问题,不要只写“出错了”这类无法判断原因的提示。
【六】生成器和yield
生成器是一种特殊的迭代器,不会一次性生成全部结果,而是在每次调用next()时计算一个结果,因此适合处理大量数据。
defnumber_generator(start,end):current=startwhilecurrent<=end:yieldcurrent current+=1numbers=number_generator(1,3)print(numbers)# 生成器对象print(next(numbers))# 1print(next(numbers))# 2print(next(numbers))# 3# print(next(numbers)) # StopIterationyield会暂停函数并保存当前执行状态,下一次调用next()时从暂停位置继续执行。使用for遍历生成器时,Python 会自动处理StopIteration。
fornumberinnumber_generator(1,3):print(number)【1】生成器表达式和列表推导式
# 列表推导式会立即生成全部数据,占用完整内存list_result=[number*2fornumberinrange(1000000)]# 生成器表达式按需生成数据,适合大数据量场景generator_result=(number*2fornumberinrange(1000000))print(next(generator_result))# 0print(next(generator_result))# 2生成器只能向前消费,已经取出的数据不会自动回到生成器中。如果需要反复遍历或随机访问,应使用列表等可以保存完整数据的容器。
【2】使用生成器逐行处理文件
defvalid_lines(file_name):withopen(file_name,"r",encoding="utf-8")asfp:forlineinfp:line=line.strip()iflineandnotline.startswith("#"):yieldlineforlineinvalid_lines("config.txt"):print(line)上面的函数只在外部循环请求数据时读取下一行,不会把整个文件一次性放进内存。with语句仍然能够在生成器结束或遍历中断时关闭文件。
【七】综合案例:统计日志中的错误信息
fromcollectionsimportCounterdeferror_levels(file_name):"""逐行提取日志中的错误级别。"""withopen(file_name,"r",encoding="utf-8")asfp:forlineinfp:parts=line.strip().split()iflen(parts)>=3andparts[2]in{"INFO","WARNING","ERROR"}:yieldparts[2]level_counter=Counter(error_levels("app.log"))print(dict(level_counter))假设app.log每行格式类似2026-07-16 ERROR database failed。实际项目中需要根据真实日志格式调整字段位置,并处理文件不存在、编码错误和损坏行等情况。
【八】常见错误总结
# 1. 忘记关闭文件,可能造成资源泄漏# fp = open("demo.txt", "r", encoding="utf-8")# 推荐使用 with 自动关闭文件withopen("demo.txt","r",encoding="utf-8")asfp:content=fp.read()# 2. 读取文件后再次 read,结果为空,因为指针已经在文件末尾withopen("demo.txt","r",encoding="utf-8")asfp:first=fp.read()second=fp.read()print(second)# ''# 需要重新读取时移动回开头withopen("demo.txt","r",encoding="utf-8")asfp:fp.read()fp.seek(0)print(fp.read())# 3. 生成器不能用下标访问numbers=(numberfornumberinrange(3))# print(numbers[0]) # TypeError# 4. 不要修改遍历中的列表,可能跳过元素values=[1,2,3,4]values=[valueforvalueinvaluesifvalue%2==0]print(values)【九】本章小结
read()会移动文件指针,seek()可以重新定位,tell()可以查看当前位置。- 文本文件应明确指定编码,二进制文件使用
rb、wb等模式。 - 异常捕获要具体,资源管理优先使用
with。 raise用于主动抛出异常,自定义异常适合表达业务错误。- 生成器通过
yield按需产生数据,适合处理大量数据和大文件。