34 KiB
brief, sources
| brief | sources | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 文件读写是 Python 程序从文本、日志或流式来源获取并处理数据的基础能力。 |
|
文件读写
学习目标
学习“文件读写”时,应掌握如何在真实的 Python 脚本环境中读取、处理和写入文件。对于 Practical Python Programming 课程而言,文件读写不是孤立语法点,而是贯穿课程练习的基础工作方式:大量程序都从 Data/ 目录读取文本数据,逐行解析,转换类型,并计算结果;后续还会把文件读取逻辑抽象成可复用函数、生成器和数据处理管道。
完成本主题后,应能够:
- 在本地文件系统中定位数据文件;
- 使用
open()打开文本文件,并理解常见文件模式; - 使用
with语句自动关闭文件; - 一次性读取整个文件,或逐行读取大文件;
- 使用
next()手动读取或跳过单行,例如跳过 CSV 表头; - 使用
readline()在特殊场景下探测文件末尾是否有新增内容; - 使用
seek()移动文件指针,例如跳到日志文件末尾; - 将文件中的字符串字段转换为整数、浮点数等类型;
- 将处理结果输出到屏幕或写入新文件;
- 理解相对路径与当前工作目录的关系;
- 在终端或 shell 中运行处理文件的 Python 脚本;
- 将课程代码组织在指定目录中,方便后续练习复用和重构;
- 理解“文件名”和“文件对象/可迭代对象”作为函数参数时的设计差异;
- 使用面向 文件类对象、可迭代对象 和 concepts/鸭子类型 的方式设计更灵活的读取函数;
- 使用 生成器 把文件读取、筛选或实时跟踪逻辑封装成可复用的迭代工具;
- 理解文件对象与 迭代协议 的关系;
- 避免因交互式环境与真实脚本环境差异导致的路径和模块问题。
前置知识
学习文件读写前,建议先具备以下基础:
- 已安装 Python 3.6 或更新版本;
- 能使用编辑器创建
.py文件; - 能在 shell 或终端中运行 Python 程序;
- 了解基本的目录、文件名和路径概念;
- 了解简单变量、字符串、列表和循环;
- 了解字符串方法,例如
split()、strip(); - 了解基本类型转换,例如
int()和float(); - 了解
for循环和next()的基本用法; - 对 Python 程序组织 有初步认识,例如脚本、函数、模块和 import;
- 对 函数抽象、接口设计 和 迭代协议 有初步认识会更有帮助。
Practical Python Programming 课程不依赖第三方包,也不要求特定操作系统、IDE 或编辑器。但它假设学习者能在本地文件系统中创建程序、访问数据文件,并从终端运行脚本。
核心解释
文件读写指程序与文件或“类文件对象”之间的数据交换。常见任务包括:
- 从文本文件读取数据;
- 按行解析文件内容;
- 跳过文件头部或读取特定行;
- 将文件中的字符串转换为数字、日期或结构化记录;
- 将处理结果输出到屏幕或写入新文件;
- 在多个程序文件之间复用读取逻辑;
- 处理普通文本文件之外的输入来源,例如 gzip 压缩文件、标准输入或字符串列表;
- 持续监控正在增长的日志文件或行情文件;
- 把文件读取模式封装成 生成器,供
for循环自然消费。
在本课程中,文件读写尤其重要,因为大量练习都围绕“从文件读取数据并编写小程序处理数据”展开。课程仓库中通常会有如下工作结构:
Work/:学习者编写代码和完成练习的主要目录;Work/Data/:课程使用的数据文件和相关脚本;Solutions/:部分练习的参考解答。
课程练习默认你在 Work/ 目录中创建和运行程序,并经常访问 Data/ 目录中的文件。因此,理解“程序从哪里运行”和“相对路径从哪里开始计算”非常关键。
例如,如果你在 Work/ 目录运行程序,而数据文件位于 Work/Data/portfolio.csv,程序中通常可以使用:
filename = 'Data/portfolio.csv'
如果你从其他目录运行同一个脚本,相对路径可能会失效。这类问题是学习文件读写时最常见的困惑之一。
打开、读取、写入和关闭文件
Python 使用内置函数 open() 打开文件。常见写法如下:
f = open('foo.txt', 'rt') # 以文本模式读取
g = open('bar.txt', 'wt') # 以文本模式写入
常见模式包括:
'rt':read text,文本读取模式;'wt':write text,文本写入模式,会覆盖已有文件;'a':append,追加写入模式;'r'、'w':在普通文本文件中也常见,但课程示例更明确使用'rt'和'wt'。
读取整个文件:
data = f.read()
也可以限制读取的最大字节数或字符数:
data = f.read(maxbytes)
写入文本:
g.write('some text')
使用完文件后应关闭:
f.close()
g.close()
不过手动关闭容易遗漏,因此推荐使用 with 语句。
使用 with 自动管理文件
推荐写法:
with open(filename, 'rt') as file:
# 使用 file
...
当控制流离开缩进代码块时,文件会自动关闭,不需要显式调用 close()。这体现了 Python 的 concepts/上下文管理器 机制,是处理文件资源的标准做法。
不推荐:
f = open('Data/example.txt')
data = f.read()
# 容易忘记 f.close()
推荐:
with open('Data/example.txt', 'rt') as f:
data = f.read()
常见读取方式
一次性读取整个文件
with open('Data/example.txt', 'rt') as f:
data = f.read()
print(data)
这种方式简单,适合小文件。但如果文件很大,一次性读入全部内容会占用较多内存,因此不总是最佳选择。
在交互式解释器中,直接输入变量名和使用 print() 的效果不同:
>>> data
'name,shares,price\n"AA",100,32.20\n'
>>> print(data)
name,shares,price
"AA",100,32.20
直接输入变量名时,Python 显示字符串的原始表示,包括引号和转义字符;使用 print(data) 时,看到的是字符串的实际格式化输出。这一点有助于理解 字符串 的表示形式与打印结果之间的区别。
逐行读取文件
with open('Data/example.txt', 'rt') as f:
for line in f:
print(line, end='')
文件对象可以直接用于 for 循环。循环会持续读取下一行,直到文件结束。逐行读取适合处理较大的文本文件,也适合 CSV、日志、配置文件等结构化或半结构化数据。
这背后依赖文件对象实现的 迭代协议。从使用者角度看,文件就是一个逐行产生字符串的 可迭代对象。
print(line, end='') 中的 end='' 常用于避免额外输出空行,因为从文件读出的每一行通常已经包含行尾换行符 \n。
使用 next() 读取或跳过单行
如果需要手动读取一行,例如跳过 CSV 文件的表头,可以使用 next():
with open('Data/portfolio.csv', 'rt') as f:
headers = next(f)
for line in f:
print(line, end='')
next(f) 返回文件中的下一行。如果反复调用,会依次得到后续行。实际上,for line in f 内部也在不断调用 next(),所以通常不需要手动调用它,除非要显式读取或跳过某一行。
使用 readline() 读取下一行
readline() 也可以读取文件中的下一行:
line = f.readline()
在普通文件处理中,更常见、更推荐的方式是直接写:
for line in f:
...
不过在监控持续增长的文件时,readline() 有特殊用途:如果当前已经读到文件末尾,它会返回空字符串 '';程序可以稍等片刻后再次尝试,从而探测是否有新内容追加。这种模式常用于日志监控,类似 Unix 的 tail -f。
常见写入方式
使用 write() 写入字符串
with open('Data/output.txt', 'wt') as f:
f.write('Hello, world\n')
f.write('This is another line\n')
注意:使用 'w' 或 'wt' 模式会覆盖已有文件。如果需要追加内容,应使用 'a' 模式。
将 print() 输出重定向到文件
with open('Data/output.txt', 'wt') as out:
print('Hello World', file=out)
这说明 print() 不只能输出到终端,也可以通过 file= 参数输出到文件对象。对于格式化输出,print(..., file=out) 经常比手动拼接字符串再调用 write() 更方便。
CSV 风格数据处理
课程早期大量使用 Data/portfolio.csv 作为示例。该文件内容类似:
name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
读取 CSV 风格文本的一种基础方式是:
with open('Data/portfolio.csv', 'rt') as f:
headers = next(f).split(',')
for line in f:
row = line.split(',')
print(row)
输出结果类似:
['"AA"', '100', '32.20\n']
这类模式在课程早期的数据处理练习中很常见:
- 打开文件;
- 跳过或解析表头;
- 逐行读取;
- 使用
split()或csv模块拆分字段; - 使用
strip()清理行尾换行符或多余空白; - 将字符串转换为合适的数据类型;
- 存入列表、字典或其他结构;
- 执行计算或输出结果。
更常见的清理写法是:
with open('Data/portfolio.csv', 'rt') as f:
headers = next(f).strip().split(',')
for line in f:
row = line.strip().split(',')
name = row[0]
shares = int(row[1])
price = float(row[2])
print(name, shares, price)
这属于 数据处理 的基础步骤:先从文件获得原始文本,再通过字符串处理和类型转换把文本变成可计算的数据。
示例任务:计算投资组合总成本
portfolio.csv 的列通常表示:
name:股票名称;shares:股数;price:购买价格。
练习 pcost.py 要求读取该文件,并计算购买所有股票的总成本。核心计算是:
cost = shares * price
示例程序结构:
total = 0.0
with open('Data/portfolio.csv', 'rt') as f:
headers = next(f)
for line in f:
row = line.strip().split(',')
shares = int(row[1])
price = float(row[2])
total += shares * price
print('Total cost', total)
示例输出:
Total cost 44671.15
这个练习把文件读取、跳过表头、逐行循环、字符串拆分、类型转换和累加计算连接在一起,是后续封装函数、重构模块和更复杂数据处理的基础。
用生成器封装文件读取模式
随着课程进入迭代协议和生成器,文件读写不再只是“打开文件并循环”。一个重要思想是:如果某种读取或筛选逻辑本质上是在不断产生数据,就可以把它封装成 生成器。
例如,搜索文件中包含某个子串的行:
def filematch(filename, substr):
with open(filename, 'r') as f:
for line in f:
if substr in line:
yield line
使用方式与普通文件迭代完全一致:
for line in filematch('Data/portfolio.csv', 'IBM'):
print(line, end='')
这种写法的意义在于:
- 文件打开、逐行读取和筛选逻辑被隐藏在函数内部;
- 调用者仍然可以用
for循环自然消费结果; - 数据是按需产生的,不需要一次性读入全部内容;
- 读取函数可以成为可复用的小工具。
这里的 yield 会产出一行并暂停函数,下一次迭代时从暂停处继续执行。它与文件对象本身一样,体现了 迭代协议 和 惰性求值 的思想。
监控持续增长的文件:类似 tail -f
文件读写还可以用于实时监控数据源。课程中的 Data/stocksim.py 会不断向 Data/stocklog.csv 写入模拟股票行情。另一个程序可以打开该文件,跳到文件末尾,然后持续等待新增行。
核心模式如下:
import os
import time
f = open('Data/stocklog.csv')
f.seek(0, os.SEEK_END) # 移动到文件末尾
while True:
line = f.readline()
if line == '':
time.sleep(0.1)
continue
print(line, end='')
这里有几个关键点:
seek(0, os.SEEK_END)把文件指针移动到当前文件末尾;readline()尝试读取下一行;- 如果暂时没有新数据,
readline()返回空字符串; - 程序短暂休眠后继续尝试;
- 当其他程序向文件追加新行时,当前程序就能读取并处理。
这种模式适合日志文件、服务器输出、调试日志、行情数据等 流式数据 来源。它不同于普通的“读取已有文件”,因为程序关注的是未来追加的数据。
follow():把实时文件跟踪封装成生成器
课程进一步要求把上述文件跟踪逻辑抽取成通用生成器 follow(filename):
import os
import time
def follow(filename):
f = open(filename)
f.seek(0, os.SEEK_END)
while True:
line = f.readline()
if line == '':
time.sleep(0.1)
continue
yield line
于是,消费者代码可以写得非常简单:
for line in follow('Data/stocklog.csv'):
print(line, end='')
这体现了一个非常重要的设计转变:
follow()负责生产数据行;for循环中的代码负责消费数据行;- 文件读取模式和业务处理逻辑被分离;
- 同一个
follow()可以用于股票行情、服务器日志、调试日志等不同场景。
在股票行情示例中,消费端可以解析每一行并只显示下跌股票:
if __name__ == '__main__':
for line in follow('Data/stocklog.csv'):
fields = line.split(',')
name = fields[0].strip('"')
price = float(fields[1])
change = float(fields[4])
if change < 0:
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
也可以结合投资组合数据,只显示自己持有的股票:
if __name__ == '__main__':
import report
portfolio = report.read_portfolio('Data/portfolio.csv')
for line in follow('Data/stocklog.csv'):
fields = line.split(',')
name = fields[0].strip('"')
price = float(fields[1])
change = float(fields[4])
if name in portfolio:
print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')
这里要求 Portfolio 类支持 in 运算符,即实现 __contains__()。这说明文件读写常常与 容器协议、迭代协议 和业务对象设计结合在一起。
follow() 也是后续 concepts/生产者消费者模式 和 生成器管道 的基础例子:一个组件负责持续产生数据,另一个组件负责过滤、解析、统计或显示。
文件名参数 vs 文件对象参数
课程后续进一步讨论了一个重要的设计问题:读取函数到底应该接收“文件名”,还是接收“已经可迭代的行对象”?
一种常见写法是让函数接收文件名,并在函数内部打开文件:
def read_data(filename):
records = []
with open(filename) as f:
for line in f:
...
records.append(r)
return records
data = read_data('file.csv')
另一种写法是让函数接收行序列或文件类对象:
def read_data(lines):
records = []
for line in lines:
...
records.append(r)
return records
with open('file.csv') as f:
data = read_data(f)
两种写法可以产生相同输出,但设计含义不同:
- 接收文件名的函数更方便直接调用,但绑定到“路径 + 普通文件打开”这一具体场景;
- 接收可迭代行对象的函数更通用,只要求参数能被逐行迭代;
- 后者更符合 concepts/鸭子类型:只要对象“表现得像一串文本行”,函数就可以处理它;
- 对于可复用代码库,后者通常更符合 库设计 和 接口设计 的最佳实践。
这是一种重要的抽象转变:函数真正需要的不是“文件名”,而是“可逐行读取的文本”。
文件类对象、可迭代对象与鸭子类型
并非所有输入都必须来自普通文本文件。Python 中很多对象都可以表现得“像文件一样”,只要它们支持类似的读取接口,例如逐行迭代。
例如,gzip 压缩文件不能直接用普通 open() 按文本内容读取,但可以使用标准库 gzip:
import gzip
with gzip.open('Data/portfolio.csv.gz', 'rt') as f:
for line in f:
print(line, end='')
这里的 'rt' 非常重要,表示以文本模式读取压缩文件。如果省略文本模式,可能读到的是字节字符串,而不是普通文本字符串。
如果读取函数面向“可迭代的行”来设计,同一个函数就可以处理多种来源:
# 普通 CSV 文件
lines = open('data.csv')
data = read_data(lines)
# gzip 压缩文件
lines = gzip.open('data.csv.gz', 'rt')
data = read_data(lines)
# 标准输入
lines = sys.stdin
data = read_data(lines)
# 字符串列表,常用于测试
lines = ['ACME,50,91.1', 'IBM,75,123.45']
data = read_data(lines)
# 生成器,例如 filematch() 或 follow()
lines = filematch('Data/portfolio.csv', 'IBM')
data = read_data(lines)
这说明,文件读写的关键不只是“磁盘文件”,还包括统一的文件接口、文件类对象、可迭代对象、生成器 和 concepts/鸭子类型 思想。
将 parse_csv() 设计为接收文件类对象
课程中的 fileparse.py 包含一个 parse_csv() 函数。早期版本可能这样使用:
portfolio = fileparse.parse_csv('Data/portfolio.csv', types=[str, int, float])
也就是说,函数接收文件名,并在内部打开文件。后续设计讨论建议把它改为接收任意文件类对象或可迭代行对象:
import gzip
import fileparse
with gzip.open('Data/portfolio.csv.gz', 'rt') as file:
port = fileparse.parse_csv(file, types=[str, int, float])
也可以直接传入字符串列表:
lines = [
'name,shares,price',
'AA,100,34.23',
'IBM,50,91.1',
'HPE,75,45.1'
]
port = fileparse.parse_csv(lines, types=[str, int, float])
这种设计还可以接收生成器产生的行。例如,先用 filematch() 筛选文件,再把结果交给解析函数;或者用 follow() 持续产生新增日志行,再在消费端解析。
这样做的好处包括:
parse_csv()不再关心数据来自普通文件、压缩文件、标准输入、内存列表还是生成器;- 单元测试更容易,因为可以直接传入少量字符串列表;
- 上层函数可以决定如何打开文件,底层函数只负责解析行;
- 文件读取、筛选、实时跟踪和业务解析可以组合;
- 代码职责更清晰,更符合 函数抽象。
不过这种灵活性也带来一个陷阱:字符串本身也是可迭代对象。
如果修改后的 parse_csv() 仍然被这样调用:
port = fileparse.parse_csv('Data/portfolio.csv', types=[str, int, float])
函数可能会把文件名字符串当作字符序列来迭代,而不是打开这个路径。结果会非常混乱,因为它会逐字符处理 'Data/portfolio.csv'。
因此,面向可迭代对象设计函数时,常需要加入安全检查,例如检测参数是否是字符串路径,并给出清晰错误提示。也可以采用约定:底层解析函数只接收文件类对象,负责打开文件的工作由 read_portfolio()、read_prices() 等上层函数完成。
重构现有读取函数
当 parse_csv() 改为接收文件类对象后,原本直接传文件名给它的函数需要小幅调整。例如 report.py 中的 read_portfolio() 和 read_prices() 应该负责打开文件,然后把文件对象交给 parse_csv():
def read_portfolio(filename):
with open(filename) as file:
return fileparse.parse_csv(file, types=[str, int, float])
这种重构保留了外部调用方式:
portfolio = read_portfolio('Data/portfolio.csv')
但内部结构变得更清晰:
- 上层函数处理具体文件路径;
- 底层
parse_csv()处理可迭代文本行; report.py和pcost.py等程序可以继续保持原有行为;- 后续如果输入来源变为 gzip、标准输入、测试列表或生成器,底层解析逻辑无需修改。
这体现了 Python 程序组织 中一个常见重构方向:把具体 I/O 与通用解析逻辑分离。
文件读写与生产者/消费者思维
文件读取代码经常天然分成两部分:
- 生产者:负责从文件、日志、压缩文件、标准输入或生成器中产生文本行;
- 消费者:负责解析字段、转换类型、筛选数据、计算结果或格式化输出。
普通 CSV 读取中,文件对象本身就是生产者:
for line in open('Data/portfolio.csv'):
...
生成器可以把更复杂的生产模式封装起来:
for line in filematch('Data/portfolio.csv', 'IBM'):
...
for line in follow('Data/stocklog.csv'):
...
这种分离让代码更容易复用和组合,也为后续 concepts/生产者消费者模式、生成器管道 和流式数据处理打下基础。
为什么不一开始就使用 Pandas
在实际数据科学工作中,Pandas 确实可以很方便地读取 CSV 文件。但 Practical Python Programming 课程在早期刻意使用标准 Python 手动读取 CSV,原因包括:
- 本课程重点是 Python 编程基础,不是 Pandas API;
- 文件读取是比 CSV 或 Pandas 更一般的问题;
- 手动处理 CSV 能练习循环、字符串、列表、类型转换、函数和模块;
- 标准库和内置功能足以说明文件处理的基本机制;
- 后续课程会在这些基础代码上逐步重构;
- 通过自己实现读取函数,可以更清楚地理解文件名、文件对象、可迭代对象、生成器和接口设计之间的关系。
因此,工作中可以使用 Pandas,但在课程练习中应优先掌握标准 Python 文件读写能力。
为什么不建议用 Notebook 完成文件读写练习
交互式环境如 Jupyter Notebook 适合实验代码片段,但 Practical Python Programming 课程明确更推荐使用编辑器、文件和终端完成练习。
原因包括:
- 课程重点不只是运行几行代码,而是学习完整脚本开发;
- 后续练习会涉及函数、模块、import 和多文件重构;
- 文件路径、当前工作目录、模块导入等问题在 Notebook 中容易被隐藏或变形;
- 实时文件监控、后台模拟程序、命令行运行等练习更适合终端环境;
- 真实脚本环境更接近课程设计目标和实际开发方式。
因此,文件读写应与 concepts/课程练习工作流、Python 程序组织 和终端运行程序结合起来学习。
常见错误
1. 文件路径错误
常见报错:
FileNotFoundError: [Errno 2] No such file or directory
原因通常是:
- 程序不在预期目录下运行;
- 相对路径写错;
- 文件名大小写不一致;
- 数据文件不在指定目录中。
在课程练习中,应确认代码放在 Work/ 目录,并从 Work/ 目录运行程序,再访问 Data/ 中的文件。
2. 混淆脚本所在目录和当前工作目录
Python 打开相对路径时,通常以“当前工作目录”为基准,而不是 .py 文件所在目录。
例如:
cd practical-python/Work
python program.py
此时:
open('Data/portfolio.csv')
会查找:
practical-python/Work/Data/portfolio.csv
如果你从仓库根目录或其他目录运行同一个程序,结果可能不同。
3. 忘记关闭文件
不推荐:
f = open('Data/example.txt')
data = f.read()
推荐:
with open('Data/example.txt', 'rt') as f:
data = f.read()
使用 with 可以自动管理文件资源。
4. 字符串没有转换为数字
从文件读出的内容默认是字符串。例如:
price = '32.20'
shares = '100'
如果需要计算,应转换类型:
price = float(price)
shares = int(shares)
否则可能得到类型错误,或在某些情况下得到字符串拼接、重复等非预期行为。
5. 没有处理行尾换行符
逐行读取文本时,每一行通常包含末尾的 \n。如果直接拆分或输出,可能看到类似:
['"AA"', '100', '32.20\n']
常见处理方式是:
line = line.strip()
然后再执行 split()。
6. 忘记 gzip 文本模式
读取 gzip 压缩文本时,如果没有指定 'rt',可能得到字节字符串:
with gzip.open('Data/portfolio.csv.gz') as f:
line = next(f) # 可能是 bytes
推荐:
with gzip.open('Data/portfolio.csv.gz', 'rt') as f:
line = next(f) # 普通 str
7. 把文件名误传给需要可迭代行对象的函数
如果某个函数已经被重构为接收文件对象或可迭代行对象,不应再直接传入文件名字符串:
parse_csv('Data/portfolio.csv') # 可能是错误用法
因为字符串本身可迭代,函数可能会逐字符读取文件名。更安全的写法是:
with open('Data/portfolio.csv') as file:
parse_csv(file)
或者让上层包装函数负责打开文件:
portfolio = read_portfolio('Data/portfolio.csv')
8. 在实时文件监控中使用普通 for 循环等待新增行
对于已经打开的普通文件,for line in f 很适合遍历已有内容,但不适合直接实现“到达文件末尾后继续等待未来新增行”的逻辑。实时监控文件时,应使用 readline() 反复探测,并在没有新行时短暂休眠:
line = f.readline()
if line == '':
time.sleep(0.1)
更好的方式是把这种逻辑封装到 follow() 生成器中。
9. 忘记把文件指针移动到末尾
如果要实现类似 tail -f 的行为,通常应先跳到文件末尾:
f.seek(0, os.SEEK_END)
否则程序可能会先读取文件中已有的全部历史内容,而不是只关注新追加的内容。
10. 直接依赖参考答案
课程提供 Solutions/ 目录作为提示和参考,但文件读写能力需要通过亲自编写代码建立。直接复制解答会削弱对路径、运行方式、数据解析、接口设计、生成器封装和调试过程的理解。
调试提示
调试文件读写问题时,可以按以下顺序检查:
- 确认当前目录:
import os
print(os.getcwd())
- 确认目标文件路径是否存在:
import os
print(os.path.exists('Data/portfolio.csv'))
- 打印正在打开的文件名:
filename = 'Data/portfolio.csv'
print('Reading:', filename)
- 先读取并打印少量内容:
with open(filename, 'rt') as f:
for i, line in enumerate(f):
print(line, end='')
if i == 4:
break
- 检查每一行拆分后的结果:
row = line.strip().split(',')
print(row)
- 检查类型转换前后的值:
print(row[1], type(row[1]))
shares = int(row[1])
print(shares, type(shares))
-
如果涉及模块和多文件程序,确认运行方式是否符合课程目录假设,并参考 Python 程序组织。
-
如果函数接收的是可迭代行对象,确认传入的是打开后的文件、gzip 文件、标准输入、字符串列表或生成器,而不是文件名字符串。
-
如果程序监控实时文件但没有输出,检查:
import os
print(os.path.exists('Data/stocklog.csv'))
并确认负责写入数据的后台程序正在运行。
- 如果使用
follow(),可以先只打印原始行,确认生成器确实产生数据,再加入字段解析和筛选逻辑。
推荐练习
建议按照课程顺序完成相关练习,因为后续章节会复用前面写过的文件处理代码,并逐步引入函数、模块、迭代协议、生成器和重构。
推荐练习方式:
- 在
Work/目录中创建.py文件; - 从终端运行脚本,而不是只在交互式环境中测试;
- 使用
Data/目录中的课程数据文件; - 先独立完成,再查看
Solutions/中的参考实现; - 将读取文件的逻辑逐渐封装成函数;
- 在后续练习中重构已有代码,而不是每次从零开始;
- 尝试把解析函数设计为接收文件类对象或可迭代行对象;
- 尝试用生成器封装可复用的读取、筛选和实时跟踪逻辑。
可以练习的任务包括:
- 打开一个文本文件并打印全部内容;
- 比较交互式解释器中直接显示字符串和
print()输出的差异; - 逐行读取文件并统计行数;
- 使用
next()跳过 CSV 文件表头; - 读取 CSV 文件并拆分字段;
- 将字符串字段转换为整数和浮点数;
- 计算文件中某列数据的总和;
- 编写
pcost.py计算portfolio.csv中股票购买总成本; - 使用
write()或print(..., file=...)写入输出文件; - 使用
gzip.open(..., 'rt')读取压缩文本文件; - 将读取逻辑封装为函数;
- 在多个脚本中复用同一个读取函数;
- 修改
parse_csv(),让它接收文件对象或可迭代行对象; - 用字符串列表测试 CSV 解析函数;
- 修复
read_portfolio()和read_prices(),让它们在内部打开文件并把文件对象传给解析函数; - 为误传文件名字符串的情况添加安全检查或清晰错误提示;
- 编写
filematch(filename, substr)生成器,返回包含指定子串的文件行; - 运行
Data/stocksim.py,观察Data/stocklog.csv持续增长; - 使用
seek()和readline()编写类似tail -f的文件监控程序; - 将实时文件监控逻辑封装为
follow(filename)生成器; - 用
follow()监控股票行情,只打印下跌股票; - 结合
report.read_portfolio(),只显示投资组合中的股票行情。
关联知识点
- Python 程序组织:文件读写代码常会被封装进函数和模块,并在后续练习中重构。
- concepts/课程练习工作流:课程要求在
Work/目录中编写和运行程序,并频繁访问Data/数据文件。 - concepts/上下文管理器:
with open(...) as f是文件资源管理的标准模式。 - 文件类对象:普通文件、gzip 文件、标准输入等对象可以通过类似接口读取。
- 可迭代对象:文件对象可以逐行迭代,字符串列表和生成器也可以作为行来源。
- 迭代协议:文件对象、生成器和
for循环都依赖同一套迭代机制。 - 生成器:可把文件筛选、实时跟踪等数据生产逻辑封装为可复用迭代器。
- yield:生成器通过
yield逐个产出文件行或处理结果。 - 惰性求值:逐行读取和生成器都避免一次性加载全部数据。
- 流式数据:持续增长的日志文件或行情文件可以被程序实时监控。
- 日志监控:
follow()模式可用于服务器日志、调试日志和其他追加型文本文件。 - tail f模式:通过
seek()、readline()和休眠重试实现类似 Unixtail -f的行为。 - concepts/生产者消费者模式:文件对象或生成器生产数据行,后续代码消费并处理数据。
- 生成器管道:多个生成器可以组合成读取、筛选、解析和输出的数据处理链。
- concepts/鸭子类型:读取函数可以关注对象是否“像一串文本行”,而不是关注它的具体类型。
- 接口设计:接收文件名还是接收可迭代行对象,是函数接口设计中的关键取舍。
- 库设计:可复用库函数通常应面向更抽象的输入协议,避免不必要限制。
- 函数抽象:底层解析函数负责解析行,上层函数负责打开具体文件或构造数据来源。
- 字符串:文件内容首先以字符串形式进入程序,需要理解表示、打印、拆分和清理。
- 数据处理:文件读写通常是数据处理程序的第一步。
- 容器协议:用
in判断股票是否在投资组合中时,需要对象支持包含关系。 - 调试:文件路径错误、类型转换错误、数据格式错误、实时监控无输出和接口误用都需要通过调试定位。
- Git 与课程仓库管理:将练习代码提交到个人 fork,有助于保留文件处理程序的演进历史。
对应教材来源
来源:Practical Python Programming, https://github.com/dabeaz-course/practical-python
相关文档:summaries/00_Setup、summaries/04_Strings、summaries/06_Files、summaries/06_Design_discussion、summaries/01_Iteration_protocol、summaries/02_Customizing_iteration
See also: summaries/00_Overview
See also: summaries/07_Functions
See also: summaries/01_Datatypes
See also: summaries/02_Containers
See also: summaries/04_Sequences
See also: summaries/05_Collections
See also: summaries/06_List_comprehension
See also: summaries/01_Script
See also: summaries/02_More_functions
See also: summaries/03_Error_checking
See also: summaries/04_Modules
See also: summaries/05_Main_module
See also: summaries/03_Producers_consumers
See also: summaries/04_More_generators
See also: summaries/05_Decorated_methods
See also: summaries/02_Logging
See also: summaries/Contents
See also: summaries/01_Introduction__00_Overview
See also: summaries/07_Objects