Python写文件有两个基础入口:write()写单个字符串,writelines()写字符串序列。两者都要求传入字符串或字符串序列,且都不会自动补换行符。实际开发中,内容粘连、TypeError和日志丢失,往往不是语法问题,而是对返回值、写入模式和缓冲刷新理解不到位。
一、write()的返回值与换行行为
write(s)接收一个字符串,返回本次写入的字符数。它不会自动加换行,连续写入两段文本会直接拼在一起。下面代码中,n1、n2、n3分别返回5、6、1:- with open('output.txt', 'w', encoding='utf-8') as f:
- n1 = f.write('Hello')
- n2 = f.write(' World')
- n3 = f.write('\n')
- print(n1, n2, n3)
复制代码 如果写成f.write('第一行')再f.write('第二行'),读取结果会是'第一行第二行'。需要换行时必须显式写入'\n'。
二、write()只接受字符串
write()的参数必须是str。直接执行f.write(42)或f.write([1,2,3])会抛出TypeError: write() argument must be str, not int或类似错误。常见做法是用f-string转换,或用join、map处理列表,也可以用json.dumps统一序列化:- import json
- value = 42
- name = '张三'
- data = {'name': '张三', 'active': True}
- with open('data.txt', 'w', encoding='utf-8') as f:
- # f.write(42) 会报 TypeError: write() argument must be str, not int
- f.write(f'姓名: {name}\n')
- f.write(f'数值: {value}\n')
- f.write(json.dumps(data, ensure_ascii=False, indent=2))
复制代码 格式化写入适合生成报表。f-string支持宽度和对齐,例如{name:<10}表示左对齐占10个字符,{salary:>10,.0f}表示右对齐并带千分位。原文的报表函数就是按标题、表头、数据行、汇总行逐段write(),累计total_salary后输出总计。
三、writelines()的参数与换行陷阱
writelines(seq)接收字符串可迭代对象,返回None。它同样不会自动添加换行符。如果传入['行1','行2','行3'],文件内容会变成“行1行2行3”。需要先给每个元素补'\n':- lines = ['行1', '行2', '行3']
- with open('bad.txt', 'w', encoding='utf-8') as f:
- f.writelines(lines) # 内容为 行1行2行3
- lines_with_newline = [line + '\n' for line in lines]
- with open('good.txt', 'w', encoding='utf-8') as f:
- f.writelines(lines_with_newline)
复制代码 writelines()和循环write()效果相同,但writelines()可以配合生成器逐条产生行,不必先把全部内容加载到内存。下面的生成器只逐行yield,适合数据量较大的报告输出:- def generate_report_lines(data):
- yield '=' * 40 + '\n'
- yield '数据报告\n'
- for i, item in enumerate(data, 1):
- yield f'{i}. {item}\n'
- yield f'总计: {len(data)}条\n'
- data = ['项目A', '项目B']
- with open('report.txt', 'w', encoding='utf-8') as f:
- f.writelines(generate_report_lines(data))
复制代码 需要确保迭代对象中的每个元素都是字符串;writelines()只是批量写入,不会替调用方做类型转换。
四、print(..., file=f)的便利性
print()可以通过file参数写入文件,并且会自动添加换行符,还能用sep控制分隔符。对于逐行输出字典数据的场景,比手动拼接'\n'更直接:- data = [{'name': '张三', 'age': 25}]
- with open('print_output.txt', 'w', encoding='utf-8') as f:
- for user in data:
- name = user['name']
- age = user['age']
- print(f'姓名: {name}, 年龄: {age}', file=f)
- print('A', 'B', 'C', sep='|', file=f)
复制代码 三者的关键差异可以这样记:write(s)参数为字符串,返回写入字符数,不自动换行;writelines(seq)参数为字符串序列,返回None,不自动换行;print(..., file=f)可写任意对象,返回None,自动换行。
五、安全写入:临时文件加os.replace
重要数据写入时,如果程序在写一半时崩溃,原文件可能损坏。原文给出的safe_write()先把内容写入filepath + '.tmp',调用f.flush()和os.fsync(f.fileno())刷新到磁盘,再用os.replace(temp_path, filepath)做替换。异常时删除临时文件并raise,避免吞掉错误:- import os
- def safe_write(filepath, content):
- temp_path = filepath + '.tmp'
- try:
- with open(temp_path, 'w', encoding='utf-8') as f:
- f.write(content)
- f.flush()
- os.fsync(f.fileno())
- os.replace(temp_path, filepath)
- except Exception:
- if os.path.exists(temp_path):
- os.remove(temp_path)
- raise
复制代码 这里的适用场景是配置文件、关键数据文件或需要避免半写状态的文件。普通临时输出可以直接用with open(),但重要文件应采用临时文件加替换的流程。
六、追加写入与简易日志器
日志需要保留旧内容时,应使用a模式追加,而不是w模式覆盖。SimpleLogger在每次log()中生成时间戳和级别,写入后调用flush(),让日志尽快落盘:- from datetime import datetime
- class SimpleLogger:
- def __init__(self, filepath):
- self.filepath = filepath
- def log(self, message, level='INFO'):
- timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- line = f'[{timestamp}] [{level}] {message}\n'
- with open(self.filepath, 'a', encoding='utf-8') as f:
- f.write(line)
- f.flush()
- def info(self, message):
- self.log(message, 'INFO')
- def error(self, message):
- self.log(message, 'ERROR')
复制代码 调用logger.info('服务启动')、logger.error('数据库连接超时')时,文件会按时间顺序追加,不会覆盖旧日志。
总结:写文件的核心是三件事。第一,始终用with open(...)管理文件句柄;第二,记住write()和writelines()都不自动换行,需要手动补'\n';第三,重要数据先写临时文件,再用os.replace()替换原文件。涉及日志时使用a模式并配合flush(),涉及多行输出时可用生成器配合writelines()控制内存占用。 |