7. 輸入和輸出
*************

有數種方式可以顯示程式的輸出；資料可以以人類易讀的形式印出，或是寫入檔
案以供未來所使用。這章節會討論幾種不同的方式。


7.1. 更華麗的輸出格式
=====================

目前為止我們已經學過兩種寫值的方式：*表示式陳述 (expression
statements)* 與 "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
     >>> no_votes = 43_132_495
     >>> percentage = yes_votes / (yes_votes + no_votes)
     >>> '{:-9} YES votes  {:2.2%}'.format(yes_votes, percentage)
     ' 42572654 YES votes  49.67%'

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

当你不需要花哨的输出而只是想快速显示某些变量以进行调试时，可以使用
"repr()" or "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. 格式化的字串文本 (Formatted String Literals)
---------------------------------------------------

格式化字符串字面值 （常简称为 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'.

有关这些格式规范的参考，请参阅参考指南 格式规格迷你语言。


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()" 结合使用时非常有用，它会返回包含所有局部变量的
字典。

例如，下面几行代码生成一组整齐的列，其中包含给定的整数和它的平方以及立
方:

   >>> 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. 旧的字符串格式化方法
---------------------------

"%" 操作符也可以用作字符串格式化。它将左边的参数解释为一个很像
"sprintf()" 风格 的格式字符串，应用到右边的参数，并返回一个由此格式化
操作产生的字符串。例如:

   >>> 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()" 返回一个 *file object*，最常用的有两个参数： "open(filename,
mode)"。

   >>> f = open('workfile', 'w')

第一个参数是包含文件名的字符串。第二个参数是另一个字符串，其中包含一些
描述文件使用方式的字符。*mode* 可以是 "'r'" ，表示文件只能读取，"'w'"
表示只能写入（已存在的同名文件会被删除），还有 "'a'" 表示打开文件以追
加内容；任何写入的数据会自动添加到文件的末尾。"'r+'" 表示打开文件进行
读写。*mode* 参数是可选的；省略时默认为 "'r'"。

通常文件是以 *text mode* 打开的，这意味着从文件中读取或写入字符串时，
都会以指定的编码方式进行编码。如果未指定编码格式，默认值与平台相关 (参
见 "open()")。在mode 中追加的 "'b'" 则以 *binary mode* 打开文件：现在
数据是以字节对象的形式进行读写的。这个模式应该用于所有不包含文本的文件
。

在文本模式下读取时，默认会把平台特定的行结束符 (Unix 上的 "\n",
Windows 上的 "\r\n") 转换为 "\n"。在文本模式下写入时，默认会把出现的
"\n" 转换回平台特定的结束符。这样在幕后修改文件数据对文本文件来说没有
问题，但是会破坏二进制数据例如 "JPEG" 或 "EXE" 文件中的数据。请一定要
注意在读写此类文件时应使用二进制模式。

在处理文件对象时，最好使用 "with" 关键字。 优点是当子句体结束后文件会
正确关闭，即使在某个时刻引发了异常。 而且使用 "with" 相比等效的
"try"-"finally" 代码块要简短得多:

   >>> with open('workfile') as f:
   ...     read_data = f.read()

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

如果你没有使用 "with" 关键字，那么你应该调用 "f.close()" 来关闭文件并
立即释放它使用的所有系统资源。如果你没有显式地关闭文件，Python的垃圾回
收器最终将销毁该对象并为你关闭打开的文件，但这个文件可能会保持打开状态
一段时间。另外一个风险是不同的Python实现会在不同的时间进行清理。

通过 "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 数据层次结构，并将它们转化为字
符串表示形式；这个过程称为 *serializing* 。从字符串表示中重建数据称为
*deserializing* 。在序列化和反序列化之间，表示对象的字符串可能已存储在
文件或数据中，或通过网络连接发送到某个远程机器。

備註:

  JSON格式通常被现代应用程序用于允许数据交换。许多程序员已经熟悉它，这
  使其成为互操作性的良好选择。

如果你有一个对象 "x" ，你可以用一行简单的代码来查看它的 JSON 字符串表
示:

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

"dumps()" 函数的另一个变体叫做 "dump()" ，它只是将对象序列化为 *text
file* 。因此，如果 "f" 是一个 *text file* 对象，我们可以这样做:

   json.dump(x, f)

要再次解码对象，如果 "f" 是一个打开的以供阅读的 *text file* 对象:

   x = json.load(f)

这种简单的序列化技术可以处理列表和字典，但是在JSON中序列化任意类的实例
需要额外的努力。 "json" 模块的参考包含对此的解释。

也參考:

  "pickle" - 封存模块

  与 JSON 不同，*pickle* 是一种允许对任意复杂 Python 对象进行序列化的
  协议。因此，它为 Python 所特有，不能用于与其他语言编写的应用程序通信
  。默认情况下它也是不安全的：如果数据是由熟练的攻击者精心设计的，则反
  序列化来自不受信任来源的 pickle 数据可以执行任意代码。
