Files
coding-mentor-agent/kb/python-course-kb-practical-python/wiki/concepts/文件读写.md
T

34 KiB
Raw Blame History

brief, sources
brief sources
文件读写是 Python 程序从文本、日志或流式来源获取并处理数据的基础能力。
summaries/07_Objects.md
summaries/01_Introduction__00_Overview.md
summaries/Contents.md
summaries/02_Logging.md
summaries/05_Decorated_methods.md
summaries/04_More_generators.md
summaries/03_Producers_consumers.md
summaries/02_Customizing_iteration.md
summaries/01_Iteration_protocol.md
summaries/06_Design_discussion.md
summaries/05_Main_module.md
summaries/04_Modules.md
summaries/03_Error_checking.md
summaries/02_More_functions.md
summaries/01_Script.md
summaries/06_List_comprehension.md
summaries/05_Collections.md
summaries/04_Sequences.md
summaries/02_Containers.md
summaries/01_Datatypes.md
summaries/07_Functions.md
summaries/06_Files.md
summaries/04_Strings.md
summaries/00_Overview.md
summaries/00_Setup.md

文件读写

学习目标

学习“文件读写”时,应掌握如何在真实的 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']

这类模式在课程早期的数据处理练习中很常见:

  1. 打开文件;
  2. 跳过或解析表头;
  3. 逐行读取;
  4. 使用 split() 或 csv 模块拆分字段;
  5. 使用 strip() 清理行尾换行符或多余空白;
  6. 将字符串转换为合适的数据类型;
  7. 存入列表、字典或其他结构;
  8. 执行计算或输出结果。

更常见的清理写法是:

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/ 目录作为提示和参考,但文件读写能力需要通过亲自编写代码建立。直接复制解答会削弱对路径、运行方式、数据解析、接口设计、生成器封装和调试过程的理解。

调试提示

调试文件读写问题时,可以按以下顺序检查:

  1. 确认当前目录:
import os
print(os.getcwd())
  1. 确认目标文件路径是否存在:
import os
print(os.path.exists('Data/portfolio.csv'))
  1. 打印正在打开的文件名:
filename = 'Data/portfolio.csv'
print('Reading:', filename)
  1. 先读取并打印少量内容:
with open(filename, 'rt') as f:
    for i, line in enumerate(f):
        print(line, end='')
        if i == 4:
            break
  1. 检查每一行拆分后的结果:
row = line.strip().split(',')
print(row)
  1. 检查类型转换前后的值:
print(row[1], type(row[1]))
shares = int(row[1])
print(shares, type(shares))
  1. 如果涉及模块和多文件程序,确认运行方式是否符合课程目录假设,并参考 Python 程序组织。

  2. 如果函数接收的是可迭代行对象,确认传入的是打开后的文件、gzip 文件、标准输入、字符串列表或生成器,而不是文件名字符串。

  3. 如果程序监控实时文件但没有输出,检查:

import os
print(os.path.exists('Data/stocklog.csv'))

并确认负责写入数据的后台程序正在运行。

  1. 如果使用 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() 和休眠重试实现类似 Unix tail -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