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,保留两位小数,并在后面加上百分号(有关详细信息,请参阅 格式规范迷你语言)。

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

当您不需要花哨的输出,而只是想快速显示一些变量以进行调试时,您可以使用 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-strings)允许您通过在字符串前添加 fF 并将表达式写为 {expression} 来在字符串中包含 Python 表达式的值。

表达式后可以跟一个可选的格式说明符。这允许更大地控制值的格式化方式。以下示例将圆周率四舍五入到小数点后三位

>>> 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 对象的形式读取和写入。在以二进制模式打开文件时,您不能指定编码

在文本模式下,读取时的默认操作是将平台特定的换行符(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() 来关闭文件并立即释放它使用的任何系统资源。

警告

调用f.write() 而不使用with 关键字或调用f.close() 可能会导致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 对象表示法) 的流行数据交换格式。名为 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 数据可能会执行任意代码。