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 模組包含一個 Template 類,該類提供了另一種將值替換為字串的方法,使用 $x 之類的佔位符,並用字典中的值替換它們,但對格式的控制較少。

7.1.1. 格式化字串字面值

格式化字串字面值(也簡稱為 f-字串)允許您透過在字串字首 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.

如果您有一個不想拆分的很長的格式字串,那麼如果您可以透過名稱而不是位置來引用要格式化的變數,那就太好了。這可以透過簡單地傳遞 dict 並使用方括號 '[]' 來訪問鍵來完成。

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

通常,檔案以文字模式開啟,這意味著,你從檔案中讀取和寫入字串,這些字串以特定的*編碼*進行編碼。 如果未指定 *encoding*,則預設值取決於平臺(請參閱 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* 個位元組(在二進位制模式下)。 如果已到達檔案末尾,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 資料可能會執行任意程式碼。