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 模块通过 string.Template 支持基于正则表达式的简单模板方法。这提供了另一种将值替换到字符串中的方式,使用 $x 等占位符,并用字典中的值替换它们。这种语法易于使用,但对格式化的控制要少得多。

7.1.1. 格式化字符串字面量

格式化字符串字面量(简称 f-string)允许您通过在字符串前加上 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.

如果你有一个非常长的格式字符串,你不想将其拆分,那么能够按名称而不是按位置引用要格式化的变量会很好。这可以通过简单地传递字典并使用方括号 '[]' 访问键来实现。

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

通常,文件以 *文本模式* 打开,这意味着您可以从文件中读写字符串,这些字符串以特定的 *编码* 进行编码。如果未指定 *编码*,则默认值取决于平台(参见 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* 个字符(在文本模式下)或 *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 数据可能会执行任意代码,如果数据是由熟练的攻击者精心构造的。