您当前位置: 首页 深入 Python 3

难度等级: ♦♦♦♢♢

文件

9 英里的步行可不是闹着玩的,尤其是在雨天。
— 哈里·凯梅尔曼,《九英里步行》

 

深入浅出

在我的 Windows 笔记本电脑上,在我安装任何应用程序之前,就有 38,493 个文件。安装 Python 3 后,这个总数又增加了近 3,000 个文件。文件是每个主要操作系统的主要存储范式;这个概念已经根深蒂固,以至于大多数人很难想象其他选择。从某种意义上说,您的计算机正在文件海洋中“溺水”。

从文本文件读取

在您可以从文件读取之前,您需要打开它。在 Python 中打开文件再简单不过了

a_file = open('examples/chinese.txt', encoding='utf-8')

Python 具有内置的 open() 函数,该函数接受文件名作为参数。这里,文件名是 'examples/chinese.txt'。这个文件名有五个有趣的地方

  1. 它不仅仅是文件名;它是目录路径和文件名的组合。一个假设的文件打开函数可能接受两个参数——目录路径和文件名——但 open() 函数只接受一个。在 Python 中,无论何时需要“文件名”,您都可以包含部分或全部目录路径。
  2. 目录路径使用正斜杠,但我没有说明我使用的操作系统。Windows 使用反斜杠来表示子目录,而 Mac OS X 和 Linux 使用正斜杠。但在 Python 中,正斜杠始终有效,即使在 Windows 上也是如此。
  3. 目录路径不以斜杠或驱动器号开头,因此它被称为相对路径。您可能会问,相对什么?耐心点,小伙子。
  4. 它是一个字符串。所有现代操作系统(甚至 Windows!)都使用 Unicode 来存储文件和目录的名称。Python 3 全面支持非 ASCII 路径名。
  5. 它不必位于您的本地磁盘上。您可能已挂载了一个网络驱动器。那个“文件”可能是完全虚拟文件系统的虚构产物。如果您的计算机将其视为文件并且可以将其作为文件访问,那么 Python 就可以打开它。

但对 open() 函数的调用并没有止步于文件名。还有一个名为 encoding 的参数。哦,天啊,这听起来令人不寒而栗.

字符编码露出其丑陋的嘴脸

字节是字节;字符是抽象的。字符串是 Unicode 字符的序列。但磁盘上的文件不是 Unicode 字符的序列;磁盘上的文件是字节的序列。因此,如果您从磁盘读取“文本文件”,那么 Python 如何将该字节序列转换为字符序列?它根据特定的字符编码算法对字节进行解码,并返回 Unicode 字符的序列(也称为字符串)。

# This example was created on Windows. Other platforms may
# behave differently, for reasons outlined below.
>>> file = open('examples/chinese.txt')
>>> a_string = file.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python31\lib\encodings\cp1252.py", line 23, in decode
    return codecs.charmap_decode(input,self.errors,decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 28: character maps to <undefined>
>>> 

刚刚发生了什么?您没有指定字符编码,因此 Python 被迫使用默认编码。什么是默认编码?如果您仔细查看回溯,您会发现它在 cp1252.py 中死亡,这意味着 Python 在这里使用 CP-1252 作为默认编码。(CP-1252 是运行 Microsoft Windows 的计算机上的常见编码。)CP-1252 字符集不支持此文件中的字符,因此读取操作会失败并出现难看的 UnicodeDecodeError

但等等,情况比这还要糟糕!默认编码是平台相关的,因此此代码可能在您的计算机上运行(如果您的默认编码是 UTF-8),但当您将其分发给其他人(他们的默认编码不同,例如 CP-1252)时,它将失败。

如果您需要获取默认字符编码,请导入 locale 模块并调用 locale.getpreferredencoding()。在我的 Windows 笔记本电脑上,它返回 'cp1252',但在楼上的 Linux 机器上,它返回 'UTF8'。我甚至无法在自己的家里保持一致!您的结果可能不同(即使在 Windows 上也是如此),具体取决于您安装的操作系统版本以及区域/语言设置的配置方式。这就是为什么每次打开文件时都指定编码如此重要的原因。

流对象

到目前为止,我们只知道 Python 有一个名为 open() 的内置函数。open() 函数返回一个流对象,该对象具有用于获取有关字符流的信息以及操作字符流的方法和属性。

>>> a_file = open('examples/chinese.txt', encoding='utf-8')
>>> a_file.name 
'examples/chinese.txt'
>>> a_file.encoding 
'utf-8'
>>> a_file.mode 
'r'
  1. name 属性反映您在打开文件时传递给 open() 函数的名称。它不会被规范化为绝对路径名。
  2. 同样,encoding 属性反映您传递给 open() 函数的编码。如果您在打开文件时没有指定编码(糟糕的开发人员!),那么 encoding 属性将反映 locale.getpreferredencoding()
  3. mode 属性告诉您以哪种模式打开了文件。您可以向 open() 函数传递一个可选的 mode 参数。您在打开此文件时没有指定模式,因此 Python 默认使用 'r',这意味着“仅以文本模式打开以供读取”。正如您将在本章后面看到的那样,文件模式有多种用途;不同的模式可以让您写入文件、追加到文件或以二进制模式打开文件(在这种模式下,您处理的是字节而不是字符串)。

open() 函数的文档列出了所有可能的 file 模式。

从文本文件读取数据

打开文件以供读取后,您可能希望在某个时候从该文件中读取。

>>> a_file = open('examples/chinese.txt', encoding='utf-8')
>>> a_file.read() 
'Dive Into Python 是为有经验的程序员编写的一本 Python 书。\n'
>>> a_file.read() 
''
  1. 打开文件(并使用正确的编码)后,读取文件只需调用流对象的 read() 方法即可。结果是一个字符串。
  2. 也许有点令人惊讶的是,再次读取文件不会引发异常。Python 并不认为读取文件末尾是错误;它只是返回一个空字符串。

如果要重新读取文件怎么办?

# continued from the previous example
>>> a_file.read() 
''
>>> a_file.seek(0) 
0
>>> a_file.read(16) 
'Dive Into Python'
>>> a_file.read(1) 
' '
>>> a_file.read(1)
'是'
>>> a_file.tell() 
20
  1. 由于您仍在文件末尾,因此对流对象的 read() 方法的进一步调用只会返回空字符串。
  2. seek() 方法移至文件中的特定字节位置。
  3. read() 方法可以接受一个可选参数,即要读取的字符数。
  4. 如果需要,您甚至可以一次读取一个字符。
  5. 16 + 1 + 1 = … 20?

让我们再试一次。

# continued from the previous example
>>> a_file.seek(17) 
17
>>> a_file.read(1) 
'是'
>>> a_file.tell() 
20
  1. 移至第 17 个字节。
  2. 读取一个字符。
  3. 现在您位于第 20 个字节。

您看到了吗?seek()tell() 方法始终计算字节,但由于您以文本方式打开了此文件,因此 read() 方法计算字符。汉字UTF-8 中需要多个字节来编码。文件中的英文字符每个只占一个字节,因此您可能会误以为 seek()read() 方法在计算相同的东西。但这只对某些字符有效。

但等等,情况更糟了!

>>> a_file.seek(18) 
18
>>> a_file.read(1) 
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    a_file.read(1)
  File "C:\Python31\lib\codecs.py", line 300, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf8' codec can't decode byte 0x98 in position 0: unexpected code byte
  1. 移至第 18 个字节,尝试读取一个字符。
  2. 为什么失败了?因为第 18 个字节没有字符。最近的字符从第 17 个字节开始(并持续三个字节)。尝试从中间读取字符会导致 UnicodeDecodeError

关闭文件

打开的文件会占用系统资源,并且根据文件模式,其他程序可能无法访问它们。重要的是,在您完成使用文件后立即将其关闭。

# continued from the previous example
>>> a_file.close()

好吧,太反高潮了。

流对象 a_file 仍然存在;调用其 close() 方法不会销毁对象本身。但这并不太有用。

# continued from the previous example
>>> a_file.read() 
Traceback (most recent call last):
  File "<pyshell#24>", line 1, in <module>
    a_file.read()
ValueError: I/O operation on closed file.
>>> a_file.seek(0) 
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    a_file.seek(0)
ValueError: I/O operation on closed file.
>>> a_file.tell() 
Traceback (most recent call last):
  File "<pyshell#26>", line 1, in <module>
    a_file.tell()
ValueError: I/O operation on closed file.
>>> a_file.close() 
>>> a_file.closed 
True
  1. 您无法从关闭的文件中读取;这会导致 IOError 异常。
  2. 您也无法在关闭的文件中查找。
  3. 关闭的文件没有当前位置,因此 tell() 方法也会失败。
  4. 也许令人惊讶的是,对已关闭文件的流对象调用 close() 方法不会引发异常。它只是一个空操作。
  5. 关闭的流对象确实有一个有用的属性:closed 属性将确认文件已关闭。

自动关闭文件

流对象具有显式的 close() 方法,但是,如果您的代码有错误,并在您调用 close() 之前崩溃怎么办?理论上,该文件可能会比必要的时间长得多。当您在本地计算机上进行调试时,这没什么大不了。但在生产服务器上,这可能就重要了。

Python 2 为此提供了一种解决方案:try..finally 块。这在 Python 3 中仍然有效,您可能在其他人的代码或 移植到 Python 3 的旧代码中看到过它。但 Python 2.6 引入了一种更简洁的解决方案,这在 Python 3 中现在是首选解决方案:with 语句。

with open('examples/chinese.txt', encoding='utf-8') as a_file:
    a_file.seek(17)
    a_character = a_file.read(1)
    print(a_character)

此代码调用 open(),但它从未调用 a_file.close()with 语句启动一个代码块,就像 if 语句或 for 循环一样。在此代码块内,您可以将变量 a_file 作为调用 open() 返回的流对象使用。所有常规的流对象方法都可用——seek()read(),任何您需要的。当 with 块结束时,Python 会自动调用 a_file.close()

关键在于:无论您如何或何时退出 with 块,Python 都会关闭该文件……即使您通过未处理的异常“退出”它。没错,即使您的代码引发异常,并且整个程序突然停止,该文件也会被关闭。保证。

从技术角度讲,with 语句创建了一个运行时上下文。在这些示例中,流对象充当上下文管理器。Python 创建流对象 a_file 并告诉它正在进入运行时上下文。当 with 代码块完成时,Python 会告诉流对象它正在退出运行时上下文,并且流对象会调用自己的 close() 方法。有关详细信息,请参阅附录 B,“可以在 with 块中使用的类”

with 语句没有与文件相关的特定内容;它只是一个用于创建运行时上下文并告诉对象它们正在进入和退出运行时上下文的通用框架。如果所讨论的对象是一个流对象,那么它会执行有用的文件类操作(例如,自动关闭文件)。但该行为是在流对象中定义的,而不是在 with 语句中定义的。还有许多其他方法可以使用与文件无关的上下文管理器。您甚至可以创建自己的上下文管理器,正如您将在本章后面看到的那样。

一次读取一行数据

文本文件的一“行”就是您所想的那样——您输入一些文字并按下 ENTER,现在您就在新的一行上。一行文本是字符的序列,以…什么分隔?好吧,这很复杂,因为文本文件可以使用多种不同的字符来标记一行的结尾。每个操作系统都有自己的约定。一些使用回车符,另一些使用换行符,还有一些在每行结尾使用这两个字符。

现在可以松一口气了,因为Python 默认情况下会自动处理行尾。如果你说:“我想一行一行地读取这个文本文件”,Python 会找出文本文件使用的行尾类型,然后一切都会正常运行。

如果你需要对被视为行尾的内容进行更细粒度的控制,可以将可选的newline参数传递给open()函数。有关所有细节,请参阅open()函数文档。

那么,你到底是怎么做的呢?也就是说,一行一行地读取文件。这太简单了,太美了。

[下载oneline.py]

line_number = 0
with open('examples/favorite-people.txt', encoding='utf-8') as a_file:  
    for a_line in a_file:                                               
        line_number += 1
        print('{:>4} {}'.format(line_number, a_line.rstrip()))          
  1. 使用with模式,你可以安全地打开文件并让 Python 为你关闭它。
  2. 要一行一行地读取文件,请使用for循环。就是这样。除了拥有read()之类的显式方法外,流对象也是一个迭代器,每次你请求一个值时,它都会吐出一个单独的行。
  3. 使用format()字符串方法,你可以打印出行号和行本身。格式说明符{:>4}表示“将此参数右对齐在 4 个空格内打印”。a_line变量包含完整行,包括回车符。rstrip()字符串方法会删除尾随空白,包括回车符。
you@localhost:~/diveintopython3$ python3 examples/oneline.py
   1 Dora
   2 Ethan
   3 Wesley
   4 John
   5 Anne
   6 Mike
   7 Chris
   8 Sarah
   9 Alex
  10 Lizzie

你遇到过这个错误吗?

you@localhost:~/diveintopython3$ python3 examples/oneline.py
Traceback (most recent call last):
  File "examples/oneline.py", line 4, in <module>
    print('{:>4} {}'.format(line_number, a_line.rstrip()))
ValueError: zero length field name in format

如果是这样,你可能正在使用 Python 3.0。你应该真正升级到 Python 3.1。

Python 3.0 支持字符串格式化,但仅支持显式编号的格式说明符。Python 3.1 允许你在格式说明符中省略参数索引。以下是与 Python 3.0 兼容的版本,供比较

print('{0:>4} {1}'.format(line_number, a_line.rstrip()))

写入文本文件

你可以像从文件中读取数据一样写入文件。首先,你打开一个文件并获取一个流对象,然后使用流对象上的方法将数据写入文件,最后你关闭文件。

要打开一个文件以进行写入,请使用open()函数并指定写入模式。有两种文件模式用于写入

这两种模式都会自动创建文件(如果文件不存在),因此无需进行任何“如果文件不存在,则创建一个新的空文件以便你第一次打开它”之类的繁琐操作。只需打开一个文件并开始写入即可。

你应该在完成写入后立即关闭文件,以释放文件句柄并确保数据实际写入磁盘。与从文件读取数据一样,你可以调用流对象的close()方法,或者使用with语句并让 Python 为你关闭文件。我敢说你猜到我推荐哪种技术了。

>>> with open('test.log', mode='w', encoding='utf-8') as a_file: 
...  a_file.write('test succeeded') 
>>> with open('test.log', encoding='utf-8') as a_file:
...     print(a_file.read())                              
test succeeded
>>> with open('test.log', mode='a', encoding='utf-8') as a_file: 
...     a_file.write('and again')
>>> with open('test.log', encoding='utf-8') as a_file:
...     print(a_file.read())                              
test succeededand again 
  1. 你大胆地创建一个名为test.log的新文件(或覆盖现有文件),并打开该文件以进行写入。mode='w'参数表示打开文件以进行写入。是的,这与听起来一样危险。我希望你不在乎该文件以前的内容(如果有),因为这些数据现在已经消失了。
  2. 你可以使用open()函数返回的流对象的write()方法将数据添加到新打开的文件中。with块结束后,Python 会自动关闭文件。
  3. 这太有趣了,让我们再做一次。但这次,使用mode='a'将数据追加到文件而不是覆盖文件。追加永远不会损害文件现有内容。
  4. 你写入的原始行和追加的第二行现在都在test.log文件中。还要注意,没有包含回车符或换行符。由于你两次都没有显式地将它们写入文件,因此文件中不包含它们。你可以使用'\r'字符写入回车符,和/或使用'\n'字符写入换行符。由于你没有这样做,因此你写入文件的所有内容都放在一行上。

字符编码再次

你注意到当你打开文件以进行写入时传递给open()函数的encoding参数了吗?它很重要;永远不要省略它!如本章开头所述,文件不包含字符串,它们包含字节。从文本文件读取“字符串”之所以可以正常工作,是因为你告诉 Python 使用什么编码来读取字节流并将其转换为字符串。将文本写入文件会反向出现相同的问题。你无法将字符写入文件;字符是一种抽象。为了写入文件,Python 需要知道如何将你的字符串转换为字节序列。确保 Python 执行正确转换的唯一方法是在你打开文件以进行写入时指定encoding参数。

二进制文件

my dog Beauregard

并非所有文件都包含文本。有些文件包含我狗的照片。

>>> an_image = open('examples/beauregard.jpg', mode='rb') 
>>> an_image.mode 
'rb'
>>> an_image.name 
'examples/beauregard.jpg'
>>> an_image.encoding 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: '_io.BufferedReader' object has no attribute 'encoding'
  1. 以二进制模式打开文件很简单,但也有一些细微差别。与以文本模式打开文件的唯一区别是,mode参数包含'b'字符。
  2. 你从以二进制模式打开文件中获取的流对象具有许多相同的属性,包括mode,它反映了你传递给open()函数的mode参数。
  3. 二进制流对象也具有name属性,就像文本流对象一样。
  4. 不过,这里有一个区别:二进制流对象没有encoding属性。这有道理,对吧?你正在读取(或写入)字节,而不是字符串,因此 Python 无需执行任何转换。你从二进制文件中获取的内容与你写入的内容完全相同,无需进行任何转换。

我是否提到过你正在读取字节?哦,是的,你确实在读取。

# continued from the previous example
>>> an_image.tell()
0
>>> data = an_image.read(3) 
>>> data
b'\xff\xd8\xff'
>>> type(data) 
<class 'bytes'>
>>> an_image.tell() 
3
>>> an_image.seek(0)
0
>>> data = an_image.read()
>>> len(data)
3150
  1. 与文本文件一样,你也可以一次读取一部分二进制文件。但有一个关键区别……
  2. ……你正在读取字节,而不是字符串。由于你以二进制模式打开了文件,因此read()方法接受要读取的字节数,而不是字符数。
  3. 这意味着你传递给read()方法的数字与你从tell()方法中获取的位置索引之间永远不会出现意外的不匹配read()方法读取字节,seek()tell()方法跟踪读取的字节数。对于二进制文件,它们总是会一致。

来自非文件源的流对象

假设你正在编写一个库,并且其中一个库函数将从文件中读取一些数据。该函数可以简单地将文件名作为字符串,打开文件以进行读取,读取它,然后在退出之前关闭它。但你不应该这样做。相反,你的API应该接受任意流对象

在最简单的情况下,流对象是指任何具有read()方法(该方法接受可选的size参数并返回字符串)的对象。当不带size参数调用时,read()方法应该读取输入源中的所有内容并将其作为单个值返回所有数据。当带size参数调用时,它会从输入源读取指定数量的数据并返回这些数据。再次调用时,它会从上次读取的位置继续读取并返回下一部分数据。

这听起来与你从打开真实文件中获取的流对象完全一样。区别在于你没有将自己局限于真实文件。被“读取”的输入源可以是任何东西:网页、内存中的字符串,甚至另一个程序的输出。只要你的函数接受流对象并简单地调用对象的read()方法,你就可以处理任何行为类似于文件的输入源,而无需编写特定代码来处理每种类型的输入。

>>> a_string = 'PapayaWhip is the new black.'
>>> import io 
>>> a_file = io.StringIO(a_string) 
>>> a_file.read() 
'PapayaWhip is the new black.'
>>> a_file.read() 
''
>>> a_file.seek(0) 
0
>>> a_file.read(10) 
'PapayaWhip'
>>> a_file.tell()                       
10
>>> a_file.seek(18)
18
>>> a_file.read()
'new black.'
  1. io模块定义了StringIO类,你可以使用它将内存中的字符串视为文件。
  2. 要从字符串创建流对象,请创建一个io.StringIO()类的实例,并将你想要用作“文件”数据的字符串传递给它。现在你有一个流对象,并且可以使用它执行各种流操作。
  3. 调用read()方法会“读取”整个“文件”,在这种情况下,对于StringIO对象来说,它只是返回原始字符串。
  4. 与真实文件一样,再次调用read()方法会返回一个空字符串。
  5. 你可以显式地定位到字符串的开头,就像在真实文件中定位一样,方法是使用StringIO对象的seek()方法。
  6. 你还可以按块读取字符串,方法是将size参数传递给read()方法。

io.StringIO允许你将字符串视为文本文件。还有一个io.BytesIO类,它允许你将字节数组视为二进制文件。

处理压缩文件

Python 标准库包含支持读取和写入压缩文件的模块。有许多不同的压缩方案;在非 Windows 系统上,最流行的两种方案是 gzip 和 bzip2。(你可能还遇到过 PKZIP 存档和 GNU Tar 存档。Python 也为它们提供了模块。)

gzip模块允许你为读取或写入 gzip 压缩文件创建流对象。它提供的流对象支持read()方法(如果你打开它以进行读取)或write()方法(如果你打开它以进行写入)。这意味着你可以使用你已经学到的用于普通文件的方法直接读取或写入 gzip 压缩文件,而无需创建临时文件来存储解压缩的数据。

作为一项额外的优势,它也支持with语句,因此你可以在完成后让 Python 自动关闭你的 gzip 压缩文件。

you@localhost:~$ python3

>>> import gzip
>>> with gzip.open('out.log.gz', mode='wb') as z_file: 
...   z_file.write('A nine mile walk is no joke, especially in the rain.'.encode('utf-8'))
... 
>>> exit()

you@localhost:~$ ls -l out.log.gz 
-rw-r--r--  1 mark mark    79 2009-07-19 14:29 out.log.gz
you@localhost:~$ gunzip out.log.gz 
you@localhost:~$ cat out.log 
A nine mile walk is no joke, especially in the rain.
  1. 你应该始终以二进制模式打开 gzip 文件。(请注意mode参数中的'b'字符。)
  2. 我在 Linux 上构建了此示例。如果你不熟悉命令行,则此命令显示了你刚刚在 Python Shell 中创建的 gzip 压缩文件的“长列表”。此列表显示文件存在(很好),并且它长 79 个字节。这实际上大于你开始使用的字符串!gzip 文件格式包含一个固定长度的头部,其中包含有关文件的一些元数据,因此对于极小的文件来说,它效率低下。
  3. gunzip命令(发音为“gee-unzip”)会解压缩文件,并将内容存储在一个新文件中,该文件与压缩文件同名,但没有.gz文件扩展名。
  4. cat命令显示文件的内容。此文件包含你最初直接从 Python Shell 中写入压缩文件out.log.gz的字符串。

你遇到过这个错误吗?

>>> with gzip.open('out.log.gz', mode='wb') as z_file:
...         z_file.write('A nine mile walk is no joke, especially in the rain.'.encode('utf-8'))
... 
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'GzipFile' object has no attribute '__exit__'

如果是这样,你可能正在使用 Python 3.0。你应该真正升级到 Python 3.1。

Python 3.0 具有gzip模块,但它不支持将 gzip 文件对象用作上下文管理器。Python 3.1 添加了在with语句中使用 gzip 文件对象的功能。

标准输入、输出和错误

命令行高手们已经熟悉标准输入、标准输出和标准错误的概念。本节内容面向其他人。

标准输出和标准错误(通常缩写为 stdoutstderr)是内置于所有类 UNIX 系统(包括 Mac OS X 和 Linux)中的管道。当您调用 print() 函数时,您要打印的内容将被发送到 stdout 管道。当您的程序崩溃并打印出回溯信息时,它将被发送到 stderr 管道。默认情况下,这两个管道都只是连接到您正在工作的终端窗口;当您的程序打印内容时,您将在终端窗口中看到输出,而当程序崩溃时,您也会在终端窗口中看到回溯信息。在图形化的 Python Shell 中,stdoutstderr 管道默认指向您的“交互式窗口”。

>>> for i in range(3):
...  print('PapayaWhip') 
PapayaWhip
PapayaWhip
PapayaWhip
>>> import sys
>>> for i in range(3):
...  l = sys.stdout.write('is the') 
is theis theis the
>>> for i in range(3):
...  l = sys.stderr.write('new black') 
new blacknew blacknew black
  1. print() 函数,在一个循环中。这里没有惊喜。
  2. stdoutsys 模块中定义,它是一个 流对象。调用其 write() 函数将打印您提供给它的任何字符串,然后返回输出的长度。事实上,这就是 print 函数真正做的;它在您要打印的字符串末尾添加一个回车符,然后调用 sys.stdout.write
  3. 在最简单的情况下,sys.stdoutsys.stderr 将其输出发送到相同的位置:Python IDE(如果您在一个 IDE 中)或终端(如果您从命令行运行 Python)。与标准输出一样,标准错误不会为您添加回车符。如果您想要回车符,您需要写入回车符字符。

sys.stdoutsys.stderr 是流对象,但它们是只写的。尝试调用它们的 read() 方法将始终引发 IOError

>>> import sys
>>> sys.stdout.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: not readable

重定向标准输出

sys.stdoutsys.stderr 是流对象,尽管它们只支持写入。但它们不是常量,而是变量。这意味着您可以为它们分配一个新值——任何其他流对象——来重定向它们的输出。

[下载 stdout.py]

import sys

class RedirectStdoutTo:
    def __init__(self, out_new):
        self.out_new = out_new

    def __enter__(self):
        self.out_old = sys.stdout
        sys.stdout = self.out_new

    def __exit__(self, *args):
        sys.stdout = self.out_old

print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

看看这个

you@localhost:~/diveintopython3/examples$ python3 stdout.py
A
C
you@localhost:~/diveintopython3/examples$ cat out.log
B

你遇到过这个错误吗?

you@localhost:~/diveintopython3/examples$ python3 stdout.py
  File "stdout.py", line 15
    with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
                                                              ^
SyntaxError: invalid syntax

如果是这样,你可能正在使用 Python 3.0。你应该真正升级到 Python 3.1。

Python 3.0 支持 with 语句,但每个语句只能使用一个上下文管理器。Python 3.1 允许您在一个 with 语句中链接多个上下文管理器。

我们先看最后的部分。

print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

这是一个复杂的 with 语句。让我将其改写成更易于识别的东西。

with open('out.log', mode='w', encoding='utf-8') as a_file:
    with RedirectStdoutTo(a_file):
        print('B')

如重写所示,您实际上有两个 with 语句,一个嵌套在另一个的范围内。“外部” with 语句应该已经很熟悉了:它以写入模式打开名为 out.logUTF-8 编码文本文件,并将流对象分配给名为 a_file 的变量。但这里并非只有这一个奇怪的地方。

with RedirectStdoutTo(a_file):

as 子句在哪里?with 语句实际上并不需要它。就像您可以调用函数并忽略其返回值一样,您也可以有一个不将 with 上下文分配给变量的 with 语句。在本例中,您只对 RedirectStdoutTo 上下文的副作用感兴趣。

这些副作用是什么?看看 RedirectStdoutTo 类内部。此类是自定义的 上下文管理器。任何类都可以通过定义两个 特殊方法 来成为上下文管理器:__enter__()__exit__()

class RedirectStdoutTo:
    def __init__(self, out_new):    
        self.out_new = out_new

    def __enter__(self):            
        self.out_old = sys.stdout
        sys.stdout = self.out_new

    def __exit__(self, *args):      
        sys.stdout = self.out_old
  1. __init__() 方法在实例创建后立即被调用。它接受一个参数,即您想要在上下文生命周期内用作标准输出的流对象。此方法只是将流对象保存在实例变量中,以便其他方法以后可以使用它。
  2. __enter__() 方法是一个 特殊的类方法;Python 在进入上下文(即在 with 语句开始时)时调用它。此方法将 sys.stdout 的当前值保存在 self.out_old 中,然后通过将 self.out_new 分配给 sys.stdout 来重定向标准输出。
  3. __exit__() 方法是另一个特殊的类方法;Python 在退出上下文(即在 with 语句结束时)时调用它。此方法通过将保存的 self.out_old 值分配给 sys.stdout 来将标准输出恢复为其原始值。

将它们组合在一起


print('A')                                                                             
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):  
    print('B')                                                                         
print('C')                                                                             
  1. 这将打印到 IDE 的“交互式窗口”(或终端,如果从命令行运行脚本)。
  2. with 语句 接受一个以逗号分隔的上下文列表。以逗号分隔的列表充当一系列嵌套的 with 块。第一个列出的上下文是“外部”块;最后一个列出的上下文是“内部”块。第一个上下文打开一个文件;第二个上下文将 sys.stdout 重定向到在第一个上下文中创建的流对象。
  3. 由于此 print() 函数是在 with 语句创建的上下文下执行的,因此它不会打印到屏幕;它将写入 out.log 文件。
  4. with 代码块已结束。Python 已经告诉每个上下文管理器在退出上下文时执行它们应该做的任何操作。上下文管理器形成一个后进先出堆栈。在退出时,第二个上下文将 sys.stdout 更改回其原始值,然后第一个上下文关闭名为 out.log 的文件。由于标准输出已恢复为其原始值,因此调用 print() 函数将再次打印到屏幕。

重定向标准错误的工作方式完全相同,使用 sys.stderr 而不是 sys.stdout

进一步阅读

© 2001–11 Mark Pilgrim