7. 输入和输出

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

7.1. 更漂亮的输出格式

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

通常,您希望比简单地打印空格分隔的值更能控制输出的格式。有几种格式化输出的方法。

  • 要使用 格式化字符串字面值,请在开头的引号或三引号之前,以 fF 开头一个字符串。在此字符串内部,您可以在 {} 字符之间编写一个可以引用变量或字面值的 Python 表达式。

    >>> year = 2016
    >>> event = 'Referendum'
    >>> f'Results of the {year} {event}'
    'Results of the 2016 Referendum'
    
  • 字符串的 str.format() 方法需要更多的手动工作。您仍然会使用 {} 来标记将替换变量的位置,并可以提供详细的格式指令,但您还需要提供要格式化的信息。在下面的代码块中,有两个如何格式化变量的示例

    >>> yes_votes = 42_572_654
    >>> total_votes = 85_705_149
    >>> percentage = yes_votes / total_votes
    >>> '{:-9} YES votes  {:2.2%}'.format(yes_votes, percentage)
    ' 42572654 YES votes  49.67%'
    

    请注意,yes_votes 如何用空格填充,并且仅对负数使用负号。该示例还打印 percentage 乘以 100,保留 2 位小数,并后跟一个百分号(有关详细信息,请参见 格式规范迷你语言)。

  • 最后,您可以通过使用字符串切片和连接操作来创建您可以想象的任何布局来自己完成所有字符串处理。字符串类型有一些方法可以执行有用的操作,将字符串填充到给定的列宽。

当您不需要花哨的输出,而只想快速显示一些变量以进行调试时,可以使用 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'))"

string 模块包含一个 Template 类,该类提供了另一种将值替换为字符串的方法,使用 $x 之类的占位符,并用字典中的值替换它们,但对格式的控制较少。

7.1.1. 格式化字符串字面值

格式化字符串字面值(也简称为 f-字符串)允许您通过在字符串前缀 fF 并将表达式写为 {expression},在字符串中包含 Python 表达式的值。

可选的格式说明符可以跟随表达式。这可以更好地控制值的格式化方式。以下示例将 pi 四舍五入到小数点后三位

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

':' 之后传递一个整数将使该字段的最小字符宽度。这对于使列对齐很有用。

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

可以使用其他修饰符在格式化之前转换值。'!a' 应用 ascii()'!s' 应用 str(),以及 '!r' 应用 repr()

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

可以使用 = 说明符将表达式扩展为表达式的文本、等号,然后是计算后的表达式的表示形式

>>> bugs = 'roaches'
>>> count = 13
>>> area = 'living room'
>>> print(f'Debugging {bugs=} {count=} {area=}')
Debugging bugs='roaches' count=13 area='living room'

有关 = 说明符的更多信息,请参见 自文档化表达式。有关这些格式规范的参考,请参阅 格式规范迷你语言 的参考指南。

7.1.2. 字符串 format() 方法

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.

如果您有一个不想拆分的很长的格式字符串,那么如果您可以通过名称而不是位置来引用要格式化的变量,那就太好了。这可以通过简单地传递 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 字典作为关键字参数传递来完成。

>>> 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() 结合使用特别有用,该函数返回包含所有局部变量的字典

>>> table = {k: str(v) for k, v in vars().items()}
>>> message = " ".join([f'{k}: ' + '{' + k +'};' for k in table.keys()])
>>> print(message.format(**table))
__name__: __main__; __doc__: None; __package__: None; __loader__: ...

例如,以下行生成一组整齐对齐的列,给出整数及其平方和立方

>>> 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

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

7.1.3. 手动字符串格式化

这是相同的手动格式化的平方和立方表

>>> 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

(请注意,每列之间的一个空格是由 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'

7.1.4. 旧式字符串格式化

% 运算符(取模)也可用于字符串格式化。 给定 format % values (其中 *format* 是一个字符串), *format* 中的 % 转换规范会被 *values* 中的零个或多个元素替换。 此操作通常称为字符串插值。 例如

>>> 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() 返回一个 文件对象,最常与两个位置参数和一个关键字参数一起使用:open(filename, mode, encoding=None)

>>> f = open('workfile', 'w', encoding="utf-8")

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

通常,文件以文本模式打开,这意味着,你从文件中读取和写入字符串,这些字符串以特定的*编码*进行编码。 如果未指定 *encoding*,则默认值取决于平台(请参阅 open())。 因为 UTF-8 是现代事实标准,所以除非你明确知道需要使用不同的编码,否则建议使用 encoding="utf-8"。 在模式中附加 'b' 将以二进制模式打开文件。 二进制模式数据将读取和写入为 bytes 对象。 以二进制模式打开文件时,你不能指定 *encoding*。

在文本模式下,读取时的默认设置是将特定于平台的行尾(Unix 上为 \n,Windows 上为 \r\n)转换为仅 \n。 在文本模式下写入时,默认设置是将出现的 \n 转换回特定于平台的行尾。 对文件数据进行这种幕后修改对于文本文件来说很好,但会破坏二进制数据,如 JPEGEXE 文件中的数据。 读取和写入此类文件时,请务必小心使用二进制模式。

在处理文件对象时,最好使用 with 关键字。 好处是,即使在某个时候引发异常,文件也会在其套件完成后正确关闭。 使用 with 也比编写等效的 try-finally 块要短得多。

>>> with open('workfile', encoding="utf-8") as f:
...     read_data = f.read()

>>> # We can check that the file has been automatically closed.
>>> f.closed
True

如果你不使用 with 关键字,则应调用 f.close() 来关闭文件并立即释放它使用的任何系统资源。

警告

在不使用 with 关键字或调用 f.close() 的情况下调用 f.write() 可能会导致 f.write() 的参数没有完全写入磁盘,即使程序成功退出。

在文件对象通过 with 语句或通过调用 f.close() 关闭后,尝试使用该文件对象将自动失败。

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

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, whence)。 该位置是通过将 *offset* 添加到参考点来计算的;参考点由 *whence* 参数选择。 *whence* 值为 0 表示从文件开头开始测量,1 表示使用当前文件位置,2 表示使用文件末尾作为参考点。 *whence* 可以省略,默认为 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* 值都会产生未定义的行为。

文件对象还有一些其他方法,例如 isatty()truncate(),这些方法不太常用;请参阅库参考以获取文件对象的完整指南。

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

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

Python 没有让用户不断编写和调试代码来将复杂的数据类型保存到文件中,而是允许你使用称为 JSON (JavaScript Object Notation) 的流行数据交换格式。 名为 json 的标准模块可以获取 Python 数据层次结构,并将它们转换为字符串表示形式;此过程称为序列化。 从字符串表示形式重建数据称为反序列化。 在序列化和反序列化之间,表示对象的字符串可能已存储在文件或数据中,或者通过网络连接发送到某个远程计算机。

注意

JSON 格式在现代应用程序中被广泛用于数据交换。许多程序员已经熟悉它,这使其成为互操作性的一个良好选择。

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

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

dumps() 函数的另一个变体,称为 dump(),只是将对象序列化到一个 文本文件。因此,如果 f 是一个为写入而打开的 文本文件 对象,我们可以这样做

json.dump(x, f)

要再次解码对象,如果 f 是一个为读取而打开的 二进制文件文本文件 对象

x = json.load(f)

注意

JSON 文件必须以 UTF-8 编码。当打开 JSON 文件作为用于读取和写入的 文本文件 时,请使用 encoding="utf-8"

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

另请参阅

pickle - pickle 模块

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