7. 輸入和輸出

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

7.1. 更華麗的輸出格式

目前為止我們已經學過兩種寫值的方式:運算式陳述 (expression statements)print() 函式。(第三種方法是使用檔案物件的 write() 方法;標準輸出的檔案是使用 sys.stdout 來達成的。詳細的資訊請參考對應的函式庫說明。)

通常你會想要對輸出格式有更多地控制,而不是僅列印出以空格隔開的值。以下是幾種格式化輸出的方式。

  • 要使用格式化字串文本 (formatted string literals),需在字串開始前的引號或連續三個引號前加上 fF。你可以在這個字串中使用 {} 包夾 Python 的運算式,引用變數或其他字面值 (literal values)。

    >>> year = 2016
    >>> event = 'Referendum'
    >>> f'Results of the {year} {event}'
    'Results of the 2016 Referendum'
    
  • 字串的 str.format() method 需要更多手動操作。你還是可以用 {} 標示欲替代變數的位置,且可給予詳細的格式指令,但你也需提供要被格式化的資訊。

    >>> 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%'
    
  • 最後,你還可以自己用字串切片 (slicing) 和串接 (concatenation) 操作,完成所有的字串處理,建立任何你能想像的排版格式。字串型別有一些 method,能以給定的欄寬填補字串,這些運算也很有用。

如果你不需要華麗的輸出,只想快速顯示變數以進行除錯,可以用 repr()str() 函式把任何的值轉換為字串。

str() 函式的用意是回傳一個人類易讀的表示法,而 repr() 的用意是產生直譯器可讀取的表示法(如果沒有等效的語法,則造成 SyntaxError)。如果物件沒有人類易讀的特定表示法,str() 會回傳與 repr() 相同的值。有許多的值,像是數字,或 list 及 dictionary 等結構,使用這兩個函式會有相同的表示法。而字串,則較為特別,有兩種不同的表示法。

一些範例:

>>> 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 class(類別),提供了將值替代為字串的另一種方法。該方法使用 $x 佔位符號,並以 dictionary 的值進行取代,但對格式的控制明顯較少。

7.1.1. 格式化的字串文本 (Formatted String Literals)

格式化的字串文本(簡稱為 f-字串),透過在字串加入前綴 fF,並將運算式編寫為 {expression},讓你可以在字串內加入 Python 運算式的值。

格式說明符 (format specifier) 是選擇性的,寫在運算式後面,可以更好地控制值的格式化方式。以下範例將 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'.

= 說明符可用於將一個運算式擴充為該運算式的文字、一個等號、以及對該運算式求值 (evaluate) 後的表示法:

>>> bugs = 'roaches'
>>> count = 13
>>> area = 'living room'
>>> print(f'Debugging {bugs=} {count=} {area=}')
Debugging bugs='roaches' count=13 area='living room'

更多關於 = 說明符的資訊請見自文件性運算式 (self-documenting expressions)。若要參考這些格式化字串的規格,詳見 格式规格迷你语言 參考指南。

7.1.2. 字串的 format() method

str.format() method 的基本用法如下:

>>> print('We are the {} who say "{}!"'.format('knights', 'Ni'))
We are the knights who say "Ni!"

大括號及其內的字元(稱為格式欄位)會被取代為傳遞給 str.format() method 的物件。大括號中的數字表示該物件在傳遞給 str.format() method 時所在的位置。

>>> print('{0} and {1}'.format('spam', 'eggs'))
spam and eggs
>>> print('{1} and {0}'.format('spam', 'eggs'))
eggs and spam

如果在 str.format() method 中使用關鍵字引數,可以使用引數名稱去引用它們的值。

>>> 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),並用方括號 '[]' 使用鍵 (key) 來輕鬆完成。

>>> 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() 組合使用時,這種方式特別實用。該函式可以回傳一個包含所有區域變數的 dictionary。

例如,下面的程式碼產生一組排列整齊的欄,列出整數及其平方與立方:

>>> 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() method 透過在左側填補空格,使字串以給定的欄寬進行靠右對齊。類似的 method 還有 str.ljust()str.center()。這些 method 不寫入任何內容,只回傳一個新字串,如果輸入的字串太長,它們不會截斷字串,而是不做任何改變地回傳;雖然這樣會弄亂欄的編排,但這通常還是比另一種情況好,那種情況會讓值變得不正確。(如果你真的想截斷字串,可以加入像 x.ljust(n)[:n] 這樣的切片運算。)

另一種 method 是 str.zfill(),可在數值字串的左邊填補零,且能識別正負號:

>>> '12'.zfill(5)
'00012'
>>> '-3.14'.zfill(7)
'-003.14'
>>> '3.14159265359'.zfill(5)
'3.14159265359'

7.1.4. 格式化字串的舊方法

% 運算子(modulo,模數)也可用於字串格式化。在 'string' % values 中,string 中所有的 % 會被 values 的零個或多個元素所取代。此運算常被稱為字串插值 (string interpolation)。例如:

>>> 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, encoding=None)

>>> f = open('workfile', 'w', encoding="utf-8")

第一個引數是一個包含檔案名稱的字串。第二個引數是另一個字串,包含了描述檔案使用方式的幾個字元。mode'r' 時,表示以唯讀模式開啟檔案;為 'w' 時,表示以唯寫模式開啟檔案(已存在的同名檔案會被抹除);為 'a' 時,以附加內容為目的開啟檔案,任何寫入檔案的資料會自動被加入到檔案的結尾。'r+' 可以開啟檔案並進行讀取和寫入。mode 引數是選擇性的,若省略時會預設為 'r'

通常,檔案以 text mode 開啟,意即,從檔案中讀取或寫入字串時,都以特定編碼方式 encoding 進行編碼。如未指定 encoding,則預設值會取決於系統平台(見 open())。因為 UTF-8 是現時的標準,除非你很清楚該用什麼編碼,否則推薦使用 encoding="utf-8"。在 mode 後面加上 'b' 會以 binary mode(二進制模式)開啟檔案,二進制模式資料以 bytes 物件的形式被讀寫。以二進制模式開啟檔案時不可以指定 encoding

在文字模式 (text mode) 下,讀取時會預設把平台特定的行尾符號(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. 檔案物件的 method

本節其餘的範例皆假設一個名為 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 裡,可以用 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) 尋找檔案最末端是例外),且只有從 f.tell() 回傳的值,或是 0,才是有效的 offset 值。其他任何 offset 值都會產生未定義的行為。

檔案物件還有一些附加的 method,像是較不常使用的 isatty()truncate();檔案物件的完整指南詳見程式庫參考手冊。

7.2.2. 使用 json 儲存結構化資料

字串可以簡單地從檔案中被寫入和讀取。數字則稍嫌麻煩,因為 read() method 只回傳字串,這些字串必須傳遞給像 int() 這樣的函式,它接受 '123' 這樣的字串,並回傳數值 123。當你想儲存像是巢狀 list 和 dictionary(字典)等複雜的資料類型時,手動剖析 (parsing) 和序列化 (serializing) 就變得複雜。

相較於讓使用者不斷地編寫和除錯程式碼才能把複雜的資料類型儲存到檔案,Python 支援一個普及的資料交換格式,稱為 JSON (JavaScript Object Notation)。標準模組 json 可接收 Python 資料階層,並將它們轉換為字串表示法;這個過程稱為 serializing(序列化)。從字串表示法中重建資料則稱為 deserializing(反序列化)。在序列化和反序列化之間,表示物件的字串可以被儲存在檔案或資料中,或通過網路連接發送到遠端的機器。

備註

JSON 格式經常地使用於現代應用程式的資料交換。許多程序設計師早已對它耳熟能詳,使它成為提升互操作性 (interoperability) 的好選擇。

如果你有一個物件 x,只需一行簡單的程式碼即可檢視它的 JSON 字串表示法:

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

dumps() 函式有一個變體,稱為 dump(),它單純地將物件序列化為 text file。因此,如果 f 是一個為了寫入而開啟的 text file 物件,我們可以這樣做:

json.dump(x, f)

f 是一個已開啟、可讀取的 binary filetext file 物件,要再次解碼物件的話:

x = json.load(f)

備註

JSON 檔案必須以 UTF-8 格式編碼。在開啟 JSON 檔案以作為一個可讀取與寫入的 text file 時,要用 encoding="utf-8"

這種簡單的序列化技術可以處理 list 和 dictionary,但要在 JSON 中序列化任意的 class(類別)實例,則需要一些額外的工作。json 模組的參考資料包含對此的說明。

也參考

pickle - pickle 模組

JSON 不同,pickle 是一種允許對任意的複雜 Python 物件進行序列化的協定。因此,它為 Python 所特有,不能用於與其他語言編寫的應用程式溝通。在預設情況,它也是不安全的:如果資料是由手段高明的攻擊者精心設計,將這段來自於不受信任來源的 pickle 資料反序列化,可以執行任意的程式碼。