程序输出有几种显示方式;数据既可以输出供人阅读的形式,也可以写入文件备用。本章探讨一些可用的方式。
7.1. 更复杂的输出格式
到目前为止,我们已遇到两种写入值的方式:表达式语句和 print() 函数。第三种方式是使用文件对象的 write() 方法;标准输出文件可通过 sys.stdout 引用。更多信息请参阅标准库参考。
对输出格式的控制不只是打印空格分隔的值。格式化输出包括以下几种方法。
- 使用格式化字符串字面值,在字符串开头的引号或三引号前添加
f或F。在字符串中,可以在{和}之间输入引用变量或字面值的 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 模块通过 string.Template 提供基于正则表达式的简单模板方法。它使用类似 $x 的占位符,并用字典中的对应值替换。这种语法容易使用,但格式控制能力有限。
7.1.1. 格式化字符串字面值
格式化字符串字面值(简称 f 字符串)在字符串前加上 f 或 F,通过 {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() 的对象替换。花括号中的数字可引用参数的位置。
>>> print('{0} and {1}'.format('spam', 'eggs'))
spam and eggs
>>> print('{1} and {0}'.format('spam', 'eggs'))
eggs and spam
如果使用关键字参数,可以通过参数名引用其值。
>>> 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 = {'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
完整概述见格式字符串语法。
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")
第一个实参是文件名字符串。第二个实参是由几个字符组成的字符串,用来描述文件的使用方式。r 表示只读;w 表示只写(已有同名文件会被清空);a 表示追加,写入的数据自动添加到文件末尾;r+ 表示读写。mode 可省略,默认是 r。
文件通常以文本模式打开:读写的是采用指定编码的字符串。如果未指定 encoding,默认编码取决于平台。UTF-8 是现代事实上的标准,因此除非明确需要其他编码,建议使用 encoding="utf-8"。在模式中添加 b 会以二进制模式打开文件;数据作为 bytes 对象读写。二进制模式不能指定 encoding。
文本模式读取时,默认将平台特定的行结束符(Unix 的 \n、Windows 的 \r\n)转换为 \n;写入时默认将 \n 转换回平台特定的行结束符。这种后台修改对文本文件适用,但会破坏 JPEG 或 EXE 等二进制文件的数据。读写此类文件务必使用二进制模式。
处理文件对象时,最好使用 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(),即使程序成功退出,也可能导致写入内容未完全写到磁盘。
文件对象被 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) 读取一定数量的数据,返回字符串(文本模式)或 bytes 对象(二进制模式)。size 是可选的数值参数。省略 size 或其值为负时,会读取并返回全部内容;如果文件比机器内存大一倍,内存问题需要你自行承担。否则,最多读取并返回 size 个字符(文本模式)或字节(二进制模式)。到达文件末尾时,f.read() 返回空字符串 ''。
>>> f.read()
'This is the entire file.\n'
>>> f.read()
''
f.readline() 读取一行。字符串末尾保留换行符 \n;只有文件不以换行符结尾时,最后一行才省略它。因此返回值不会混淆:空字符串表示到达文件末尾;空行则是仅含换行符的字符串 '\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
其他类型的对象必须先转换成字符串(文本模式)或 bytes 对象(二进制模式)再写入:
>>> value = ('the answer', 42)
>>> s = str(value) # convert the tuple to string
>>> f.write(s)
18
f.tell() 返回一个整数,表示文件对象的当前位置。在二进制模式中,它是距文件开头的字节数;在文本模式中,它是不透明的数值。
f.seek(offset, whence) 改变文件位置。新位置由参考点加上 offset 得到。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() 返回的值或 0;其他值产生未定义行为。
文件对象还有 isatty() 和 truncate() 等使用较少的方法。完整指南见标准库参考。
7.2.2. 使用 json 保存结构化数据
字符串容易写入文件或从中读取。数字稍麻烦,因为 read() 只返回字符串,需要传给 int() 等函数,将 '123' 转换为数值 123。保存嵌套列表和字典等复杂数据时,手动解析和序列化会变得复杂。
Python 支持流行的数据交换格式 JSON(JavaScript Object Notation),让用户无需不断编写和调试复杂数据的保存代码。标准库 json 模块将有层级结构的 Python 数据转换成字符串表示,这称为序列化;从字符串表示重建数据称为反序列化。在两者之间,代表对象的字符串可以保存到文件或数据库,也可以经网络发往远端机器。
备注:现代应用常使用 JSON 交换数据,许多程序员已经熟悉它,因此它很适合实现互操作。
只需一行代码,就能查看对象的 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 模块参考解释了相关做法。
参见:pickle 模块。与 JSON 不同,pickle 允许序列化任意复杂的 Python 对象。它为 Python 专用,无法与其他语言编写的应用通信。默认情况下它也不安全:反序列化不可信来源的 pickle 数据,可能执行攻击者精心构造的任意代码。











暂无评论内容