Skip to main content

7. 输入和输出

有几种方法来呈现程序的输出;数据可以以人类可读的形式打印,或者写入文件以备将来使用。本章将讨论一些可能性。

7.1. Fancier输出格式化

到目前为止,我们遇到了两种方法来写值:表达式语句print() 函数。 (第三种方法是使用文件对象的 write() 方法;标准输出文件可以被引用为 sys.stdout。有关此方面的更多信息,请参见库参考。

通常,您只需打印空格分隔的值,就可以更好地控制输出的格式。有两种方式来格式化输出;第一种方式是做所有的字符串处理自己;使用字符串切片和连接操作,您可以创建任何可以想象的布局。字符串类型有一些方法来执行有用的操作来将字符串填充到给定的列宽;这些将在短期内讨论。第二种方法是使用 格式化的字符串文字str.format() 方法。

string 模块包含一个 Template 类,它提供了另一种将值替换为字符串的方法。

还有一个问题,当然是:如何将值转换为字符串?幸运的是,Python有办法将任何值转换为字符串:将它传递给 repr()str() 函数。

str() 函数旨在返回值相当可读的值的表示,而 repr() 意在生成可以由解释器读取的表示(或者如果没有等效语法,则将强制 SyntaxError)。对于没有用于人类消费的特定表示的对象,str() 将返回与 repr() 相同的值。许多值,例如数字或结构,如列表和字典,使用任一函数具有相同的表示。字符串,特别是有两个不同的表示。

一些例子:

>>> s = 'Hello, world.'
>>> str(s)
'Hello, world.'
>>> repr(s)
"'Hello, world.'"
>>> str(1/7)
'0.14285714285714285'
>>> x = 10 * 3.25
>>> y = 200 * 200
>>> s = 'The value of x is ' + repr(x) + ', and y is ' + repr(y) + '...'
>>> print(s)
The value of x is 32.5, and y is 40000...
>>> # The repr() of a string adds string quotes and backslashes:
... hello = 'hello, world\n'
>>> hellos = repr(hello)
>>> print(hellos)
'hello, world\n'
>>> # The argument to repr() may be any Python object:
... repr((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"

这里有两种方法来写一个正方形和立方体的表:

>>> for x in range(1, 11):
...     print(repr(x).rjust(2), repr(x*x).rjust(3), end=' ')
...     # Note use of 'end' on previous line
...     print(repr(x*x*x).rjust(4))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

>>> for x in range(1, 11):
...     print('{0:2d} {1:3d} {2:4d}'.format(x, x*x, x*x*x))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

(请注意,在第一个示例中,每个列之间的一个空格是通过 print() 工作的方式添加的:它总是在其参数之间添加空格。)

此示例演示了字符串对象的 str.rjust() 方法,该方法通过在左侧的空格填充给定宽度的字段中的右对齐字符串。有类似的方法 str.ljust()str.center()。这些方法不写任何东西,它们只是返回一个新的字符串。如果输入字符串太长,它们不截断它,但返回不变;这将弄乱你的列布局,但这通常比替代,这将是一个价值。 (如果你真的想要截断,你可以随时添加一个切片操作,如在 x.ljust(n)[:n]。)

还有另一种方法,str.zfill(),在左边用零填充一个数字字符串。它理解加号和减号:

>>> '12'.zfill(5)
'00012'
>>> '-3.14'.zfill(7)
'-003.14'
>>> '3.14159265359'.zfill(5)
'3.14159265359'

str.format() 方法的基本用法如下所示:

>>> print('We are the {} who say "{}!"'.format('knights', 'Ni'))
We are the knights who say "Ni!"

其中的括号和字符(称为格式字段)将替换为传递到 str.format() 方法的对象。括号中的数字可用于指代传递到 str.format() 方法中的对象的位置。

>>> print('{0} and {1}'.format('spam', 'eggs'))
spam and eggs
>>> print('{1} and {0}'.format('spam', 'eggs'))
eggs and spam

如果在 str.format() 方法中使用关键字参数,则通过使用参数的名称来引用它们的值。

>>> print('This {food} is {adjective}.'.format(
...       food='spam', adjective='absolutely horrible'))
This spam is absolutely horrible.

位置和关键字参数可以任意组合:

>>> print('The story of {0}, {1}, and {other}.'.format('Bill', 'Manfred',
                                                       other='Georg'))
The story of Bill, Manfred, and Georg.

在格式化之前,可以使用 '!a' (应用 ascii()),'!s' (应用 str())和 '!r' (应用 repr())来转换值:

>>> contents = 'eels'
>>> print('My hovercraft is full of {}.'.format(contents))
My hovercraft is full of eels.
>>> print('My hovercraft is full of {!r}.'.format(contents))
My hovercraft is full of 'eels'.

可选的 ':' 和格式说明符可以在字段名称后面。这允许更好地控制如何格式化值。以下示例将Pi轮转到小数点后的三个位置。

>>> import math
>>> print('The value of PI is approximately {0:.3f}.'.format(math.pi))
The value of PI is approximately 3.142.

':' 之后传递整数将导致该字段为最小字符数。这对于使表漂亮很有用。

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 7678}
>>> for name, phone in table.items():
...     print('{0:10} ==> {1:10d}'.format(name, phone))
...
Jack       ==>       4098
Dcab       ==>       7678
Sjoerd     ==>       4127

如果你有一个非常长的格式字符串,你不想分裂,这将是很好,如果你可以引用要格式化的名称而不是位置的变量。这可以通过简单地传递dict并使用方括号 '[]' 来访问键来完成

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; '
...       'Dcab: {0[Dcab]:d}'.format(table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

这也可以通过传递表作为关键字参数与’**’符号。

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

这与内置函数 vars() (返回包含所有局部变量的字典)结合使用特别有用。

有关使用 str.format() 的字符串格式化的完整概述,请参阅 格式字符串语法

7.1.1. 旧字符串格式化

% 运算符也可用于字符串格式化。它解释左参数很像一个 sprintf() -style格式字符串应用于右参数,并返回从此格式化操作得到的字符串。例如:

>>> import math
>>> print('The value of PI is approximately %5.3f.' % math.pi)
The value of PI is approximately 3.142.

更多信息可以在 printf 样式字符串格式 部分找到。

7.2. 读写文件

open() 返回 file object,最常用于两个参数:open(filename, mode)

>>> f = open('workfile', 'w')

第一个参数是一个包含文件名的字符串。第二个参数是另一个包含几个字符的字符串,用于描述文件的使用方式。 mode 可以是 'r',当文件将只被读取,'w' 仅用于写入(同名的现有文件将被擦除),'a' 打开文件进行追加;写入文件的任何数据将自动添加到结尾。 'r+' 打开用于读取和写入的文件。 mode 参数是可选的;如果省略,则假定 'r'

通常,文件在 text mode 中打开,这意味着您从文件读取和写入字符串,并以特定编码进行编码。如果未指定编码,那么缺省值取决于平台(请参阅 open())。附加到模式的 'b' 打开 binary mode 中的文件:现在以字节对象的形式读取和写入数据。此模式应用于不包含文本的所有文件。

在文本模式下,读取时的默认值是将特定于平台的行结尾(Unix上的 \n,Windows上的 \r\n)转换为仅 \n。当在文本模式下写入时,默认是将 \n 的发生转换回平台特定的行结束。这种对文件数据的幕后修改对于文本文件是很好的,但会破坏像 JPEGEXE 文件中的二进制数据。在读取和写入这样的文件时非常小心地使用二进制模式。

7.2.1. 文件对象的方法

本节中的其余示例将假定已经创建了一个名为 f 的文件对象。

要读取文件内容,请调用 f.read(size),它读取一些数据并将其作为字符串(以文本模式)或字节对象(以二进制模式)返回。 size 是可选的数字参数。当省略或取消 size 时,将读取并返回文件的整个内容;这是你的问题,如果文件是机器的内存的两倍大。否则,最多读取和返回 size 字节。如果已经到达文件的结尾,f.read() 将返回一个空字符串('')。

>>> f.read()
'This is the entire file.\n'
>>> f.read()
''

f.readline() 从文件中读取一行;换行符(\n)留在字符串的末尾,如果文件未以换行符结束,则只在文件的最后一行中省略。这使得返回值清楚;如果 f.readline() 返回一个空字符串,则已到达文件的结尾,而空行由 '\n' 表示,该字符串只包含一个换行符。

>>> f.readline()
'This is the first line of the file.\n'
>>> f.readline()
'Second line of the file\n'
>>> f.readline()
''

对于从文件中读取行,可以在文件对象上循环。这是内存高效,快速,并导致简单的代码:

>>> for line in f:
...     print(line, end='')
...
This is the first line of the file.
Second line of the file

如果要读取列表中文件的所有行,您还可以使用 list(f)f.readlines()

f.write(string)string 的内容写入文件,返回写入的字符数。

>>> f.write('This is a test\n')
15

在写入其他类型的对象之前,需要转换为字符串(在文本模式下)或字节对象(在二进制模式下):

>>> value = ('the answer', 42)
>>> s = str(value)  # convert the tuple to string
>>> f.write(s)
18

f.tell() 返回一个整数,给出文件对象在文件中的当前位置,表示为从二进制模式开始的文件开始的字节数和在文本模式下的不透明数。

要更改文件对象的位置,请使用 f.seek(offset, from_what)。通过将 offset 添加到参考点来计算位置;参考点由 from_what 参数选择。 from_what 值为0测量从文件的开头,1使用当前文件位置,和2使用文件的结尾作为参考点。 from_what 可以省略,默认为0,使用文件的开头作为参考点。

>>> f = open('workfile', 'rb+')
>>> f.write(b'0123456789abcdef')
16
>>> f.seek(5)      # Go to the 6th byte in the file
5
>>> f.read(1)
b'5'
>>> f.seek(-3, 2)  # Go to the 3rd byte before the end
13
>>> f.read(1)
b'd'

在文本文件(在模式字符串中没有 b 打开的文件)中,只允许相对于文件开头的寻找(异常寻找到 seek(0, 2) 的文件末尾),并且唯一有效的 offset 值是从 f.tell() 或零。任何其他 offset 值产生未定义的行为。

当您完成了一个文件,调用 f.close() 关闭它,并释放由打开的文件占用的任何系统资源。调用 f.close() 后,尝试使用文件对象将自动失败。

>>> f.close()
>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ValueError: I/O operation on closed file

在处理文件对象时,最好使用 with 关键字。这具有的优点是,在其套件完成后,文件被正确关闭,即使在路上出现异常。它也比编写等效的 try - finally 块短得多:

>>> with open('workfile', 'r') as f:
...     read_data = f.read()
>>> f.closed
True

文件对象具有一些附加方法,例如较不频繁使用的 isatty()truncate();请参阅库参考以获取文件对象的完整指南。

7.2.2. 使用 json 保存结构化数据

字符串可以轻松地写入和读取文件。数字需要更多的努力,因为 read() 方法只返回字符串,这将必须传递到像 int() 这样的函数,它接受一个字符串像 '123',并返回其数值123.当你想保存更复杂的数据类型嵌套列表和字典,手动解析和序列化变得复杂。

而不是使用户不断地编写和调试代码以将复杂的数据类型保存到文件,Python允许您使用名为 JSON(JavaScript对象表示法) 的流行数据交换格式。标准模块称为 json 可以获取Python数据层次结构,并将其转换为字符串表示;这个过程称为 serializing。从字符串表示重构数据称为 deserializing。在串行化和反序列化之间,表示对象的字符串可能已经存储在文件或数据中,或者通过网络连接发送到一些远程机器。

注解

JSON格式通常由现代应用程序使用以允许数据交换。许多程序员已经熟悉它,这使它成为互操作性的不错选择。

如果你有一个对象 x,你可以用一行简单的代码来查看它的JSON字符串表示:

>>> json.dumps([1, 'simple', 'list'])
'[1, "simple", "list"]'

dumps() 函数的另一个变体,称为 dump(),只是将对象序列化为 text file。所以如果 f 是一个 text file 对象打开写入,我们可以这样做:

json.dump(x, f)

要再次解码对象,如果 f 是已打开用于读取的 text file 对象:

x = json.load(f)

这种简单的序列化技术可以处理列表和字典,但是在JSON中序列化任意类实例需要一些额外的努力。 json 模块的参考资料包含对此的解释。

参见

pickle - 酱菜模块

JSON 相反,pickle 是允许任意复杂的Python对象的序列化的协议。因此,它特定于Python,不能用于与以其他语言编写的应用程序通信。默认情况下,它也是不安全的:反序列化来自不可信来源的pickle数据可以执行任意代码,如果数据是由熟练的攻击者制定的。