我应该如何在 Python 中逐行读取文件?

在史前时代(Python 1.4) ,我们这样做:

fp = open('filename.txt')
while 1:
line = fp.readline()
if not line:
break
print(line)

在 Python 2.1之后,我们做了:

for line in open('filename.txt').xreadlines():
print(line)

在我们得到 Python 2.3中方便的迭代器协议之前,我们可以做:

for line in open('filename.txt'):
print(line)

我看到过一些使用更详细的例子:

with open('filename.txt') as fp:
for line in fp:
print(line)

这是未来的首选方法吗?

[ edit ]我知道 with 语句确保了文件的关闭... ... 但是为什么文件对象的迭代器协议中没有包含这一点呢?

362869 次浏览

是的,

with open('filename.txt') as fp:
for line in fp:
print(line)

才是正确的选择。

不是更冗长,而是更安全。

有一个确切的原因,为什么下列是首选的:

with open('filename.txt') as fp:
for line in fp:
print(line)

我们都被 CPython 相对确定的垃圾收集引用计数方案所破坏。其他的,假设的 Python 实现如果使用其他方案来回收内存,那么在没有 with块的情况下不一定会“足够快”地关闭文件。

在这种实现中,如果代码打开文件的速度比垃圾收集器调用孤立文件句柄上的终结器的速度更快,则可能会从操作系统得到“打开的文件太多”错误。通常的解决办法是立即触发 GC,但这是一个讨厌的黑客行为,必须由 每个函数来完成,否则可能会遇到错误,包括库中的错误。真是个噩梦。

或者你可以直接使用 with块。

附加问题

(如果只对问题的客观方面感兴趣,现在就停止阅读。)

为什么文件对象的迭代器协议中没有包含这一点呢?

这是一个关于 API 设计的主观问题,所以我有一个主观的答案分为两部分。

从直觉上讲,这种做法是错误的,因为它让迭代器协议做两件不同的事情ーー在 还有行上迭代以关闭文件句柄ーー而且让一个看起来简单的函数做两件事通常是一个坏主意。在这种情况下,感觉特别糟糕,因为迭代器以一种准函数的、基于值的方式与文件内容相关联,但是管理文件句柄是一项完全独立的任务。对于阅读代码的人来说,将两者无形地压缩成一个动作是令人惊讶的,这使得推断程序行为变得更加困难。

其他语言基本上也得出了同样的结论。Haskell 曾经短暂地使用过所谓的“延迟 IO”,它允许你迭代一个文件,当你到达流的末尾时,它会自动关闭。但是现在在哈斯克尔,人们普遍不鼓励使用延迟 IO,而 Haskell 的用户大多转向更加明确的资源管理,比如 Conduit,它的行为更像 Python 中的 with块。

在技术层面上,您可能希望使用 Python 中的文件句柄执行一些操作,但是如果迭代关闭了文件句柄,那么这些操作将不能很好地工作。例如,假设我需要对文件进行两次迭代:

with open('filename.txt') as fp:
for line in fp:
...
fp.seek(0)
for line in fp:
...

虽然这是一个不太常见的用例,但考虑到这样一个事实,即我可能刚刚将底部的三行代码添加到现有的代码库中,而这些代码库最初只有前三行。如果迭代关闭了文件,我就不能这样做了。因此,将迭代和资源管理分开使得将代码块组合成一个更大的、可工作的 Python 程序变得更加容易。

可组合性是语言或 API 最重要的可用性特性之一。

如果你被额外的一行关掉了,你可以使用这样的包装函式:

def with_iter(iterable):
with iterable as iter:
for item in iter:
yield item


for line in with_iter(open('...')):
...

在 Python 3.3中,yield from语句会使这个过程更加简短:

def with_iter(iterable):
with iterable as iter:
yield from iter