💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### 7. 输入和输出 展现程序的输出有多种方法;可以打印成人类可读的形式,也可以写入到文件以便后面使用。本章将讨论其中的几种方法。 ### 7.1. 格式化输出 到目前为止我们遇到过两种输出值的方法:*表达式*语句和[print()](# "print")函数。(第三个方式是使用文件对象的write()方法;标准输出文件可以引用 sys.stdout。详细内容参见库参考手册。) 通常你会希望更好地控制输出的格式而不是简单地打印用空格分隔的值。有两种方法来设置输出格式;第一种方式是自己做所有的字符串处理;使用字符串切片和连接操作,你可以创建任何你能想象到的布局。字符串类型有一些方法,用于执行将字符串填充到指定列宽度的有用操作;这些稍后将讨论。第二种方法是使用[str.format()](# "str.format")方法。 [string](# "string: Common string operations.")模块包含一个[Template](# "string.Template")类,提供另外一种向字符串代入值得方法。 当然还有一个问题:如何将值转换为字符串?幸运的是,Python 有方法将任何值转换为字符串:将它传递给[repr()](# "repr")或[str()](# "str")函数。 [str()](# "str")函数的用意在于返回人类可读的表现形式,而[repr()](# "repr")的用意在于生成解释器可读的表现形式(如果没有等价的语法将会引发[SyntaxError](# "exceptions.SyntaxError")异常)。对于对人类并没有特别的表示形式的对象,[str()](# "str")和[repr()](# "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()](# "print")自动添加:它总会在它的参数之间添加空格。) 上面的例子演示了字符串对象的[str.rjust()](# "str.rjust")方法,它通过在左侧填充空格使字符串在给定宽度的列右对齐。类似的方法还有[str.ljust()](# "str.ljust")和[str.center()](# "str.center")。这些方法不会输出任何内容,它们只返回新的字符串。如果输入的字符串太长,它们不会截断字符串,而是保持原样返回;这会使列的格式变得混乱,但是通常好于另外一种选择,那可能是一个错误的值。(如果你真的想要截断,可以加上一个切片操作,例如x.ljust(n)[:n]。) 另外一种方法 [str.zfill()](# "str.zfill"),它向数值字符串左侧填充零。该函数可以正确识别正负号: ~~~ >>> '12'.zfill(5) '00012' >>> '-3.14'.zfill(7) '-003.14' >>> '3.14159265359'.zfill(5) '3.14159265359' ~~~ [str.format()](# "str.format")方法的基本用法如下所示: ~~~ >>> print('We are the {} who say "{}!"'.format('knights', 'Ni')) We are the knights who say "Ni!" ~~~ 花括号及其中的字符(称为格式字段)将被替换为传递给[str.format()](# "str.format")方法的对象。可以用括号中的数字指定传递给[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()](# "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()](# "ascii")),'!s'(用于[str()](# "str"))和'!r'(用于[repr()](# "repr"))可以用来格式化之前对值进行转换。 ~~~ >>> import math >>> print('The value of PI is approximately {}.'.format(math.pi)) The value of PI is approximately 3.14159265359. >>> print('The value of PI is approximately {!r}.'.format(math.pi)) The value of PI is approximately 3.141592653589793. ~~~ 字段名后允许可选的': '和格式指令。这允许更好地控制如何设置值的格式。下面的例子将 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 ~~~ 如果你有一个实在是很长的格式字符串但又不想分开写,要是可以按照名字而不是位置引用变量就好了。有个简单的方法,可以传入一个字典,然后使用'[]'访问。 ~~~ >>> 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()](# "vars")组合起来将更加有用,该函数返回一个包含所有局部变量的字典。 关于[str.format()](# "str.format")完整的描述,请参见[*格式字符串语法*](#)。 #### 7.1.1. 旧式的字符串格式 %运算符也可以用于字符串格式化。它将左边类似sprintf()-风格的参数应用到右边的参数,然后返回这种格式化操作生成的字符串。例如: ~~~ >>> import math >>> print('The value of PI is approximately %5.3f.' % math.pi) The value of PI is approximately 3.142. ~~~ 在[*printf-style String Formatting*](#)一节,可以找到更多的信息。 ### 7.2. 读写文件 [open()](# "open")返回一个[*文件对象*](#),最常见的用法带有两个参数:open(filename,mode)。 ~~~ >>> f = open('workfile', 'w') ~~~ 第一个参数是一个含有文件名的字符串。第二个参数也是一个字符串,含有描述如何使用该文件的几个字符。*mode*为'r'时表示只是读取文件;w 表示只是写入文件(已经存在的同名文件将被删掉);'a'表示打开文件进行追加,写入到文件中的任何数据将自动添加到末尾。'r+'表示打开文件进行读取和写入。*mode* 参数是可选的,默认为'r'。 通常,文件以*文本*打开,这意味着,你从文件读出和向文件写入的字符串会被特定的编码方式(默认是UTF-8)编码。模式后面的'b'以*二进制模式*打开文件:数据会以字节对象的形式读出和写入。这种模式应该用于所有不包含文本的文件。 在文本模式下,读取时默认会将平台有关的行结束符(Unix上是\n, Windows上是\r\n)转换为\n。在文本模式下写入时,默认会将出现的\n转换成平台有关的行结束符。这种暗地里的修改对 ASCII 文本文件没有问题,但会损坏JPEG或EXE这样的二进制文件中的数据。使用二进制模式读写此类文件时要特别小心。 #### 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) >>> f.write(s) 18 ~~~ f.tell()返回一个给出文件对象在文件中当前位置的整数,在二进制模式下表示自文件开头的比特数,在文本模式下是一个无法理解的数。 若要更改该文件对象的位置,可以使用f.seek(offset,from_what)。新的位置由参考点加上 *offse* 计算得来,参考点的选择则来自于 *from_what* 参数。*from_what* 值为 0 表示以文件的开始为参考点,1 表示以当前的文件位置为参考点,2 表示以文件的结尾为参考点。*from_what* 可以省略,默认值为 0,表示以文件的开始作为参考点。 ~~~ >>> f = open('workfile', 'r+') >>> f.write('0123456789abcdef') >>> f.seek(5) # Go to the 6th byte in the file >>> f.read(1) '5' >>> f.seek(-3, 2) # Go to the 3rd byte before the end >>> f.read(1) 'd' ~~~ 在文本文件中(没有以b模式打开),只允许从文件头开始寻找(有个例外是用seek(0,2)寻找文件的最末尾处)而且合法的*偏移*值只能是f.tell()返回的值或者是零。其它任何*偏移* 值都会产生未定义的行为。 使用完一个文件后,调用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();有关文件对象的完整指南,请参阅 Python 库参考手册。 #### 7.2.2. 使用[json](# "json: Encode and decode the JSON format.")存储结构化数据 从文件中读写字符串很容易。数值就要多费点儿周折,因为read ()方法只会返回字符串,应将其传入[int()](# "int")这样的函数,就可以将'123'这样的字符串转换为对应的数值 123。当你想要保存更为复杂的数据类型,例如嵌套的列表和字典,手工解析和序列化它们将变得更复杂。 好在用户不是非得自己编写和调试保存复杂数据类型的代码,Python 允许你使用常用的数据交换格式[JSON(JavaScript Object Notation)](http://json.org)。标准模块[json](# "json: Encode and decode the JSON format.")可以接受 Python 数据结构,并将它们转换为字符串表示形式;此过程称为*序列化*。从字符串表示形式重新构建数据结构称为*反序列化*。序列化和反序列化的过程中,表示该对象的字符串可以存储在文件或数据中,也可以通过网络连接传送给远程的机器。 注意 JSON 格式经常用于现代应用程序中进行数据交换。许多程序员都已经熟悉它了,使它成为相互协作的一个不错的选择。 如果你有一个对象x,你可以用简单的一行代码查看其 JSON 字符串表示形式: ~~~ >>> json.dumps([1, 'simple', 'list']) '[1, "simple", "list"]' ~~~ [dumps()](# "json.dumps")函数的另外一个变体[dump()](# "json.dump"),直接将对象序列化到一个[*文本文件*](#)。所以如果f是为写入而打开的一个[*文本文件*](#)对象,我们可以这样做: ~~~ json.dump(x, f) ~~~ 为了重新解码对象,如果f是为读取而打开的[*文本文件*](#)对象: ~~~ x = json.load(f) ~~~ 这种简单的序列化技术可以处理列表和字典,但序列化任意类实例为 JSON 需要一点额外的努力。[Json](# "json: Encode and decode the JSON format.")模块的手册对此有详细的解释。 另请参阅 [pickle](# "pickle: Convert Python objects to streams of bytes and back.") - pickle模块 与[*JSON*](#) 不同,*pickle* 是一个协议,它允许任意复杂的 Python 对象的序列化。因此,它只能用于 Python 而不能用来与其他语言编写的应用程序进行通信。默认情况下它也是不安全的:如果数据由熟练的攻击者精心设计, 反序列化来自一个不受信任源的 pickle 数据可以执行任意代码。