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 模組透過 string.Template 支援基於正則表示式的簡單模板方法。這提供了另一種將值替換到字串中的方式,使用 $x 等佔位符,並用字典中的值替換它們。這種語法易於使用,但對格式化的控制要少得多。

7.1.1. 格式化字串字面量

格式化字串字面量(簡稱 f-string)允許您透過在字串前加上 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.

如果你有一個非常長的格式字串,你不想將其拆分,那麼能夠按名稱而不是按位置引用要格式化的變數會很好。這可以透過簡單地傳遞字典並使用方括號 '[]' 訪問鍵來實現。

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

通常,檔案以 *文字模式* 開啟,這意味著您可以從檔案中讀寫字串,這些字串以特定的 *編碼* 進行編碼。如果未指定 *編碼*,則預設值取決於平臺(參見 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* 個字元(在文字模式下)或 *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 資料可能會執行任意程式碼,如果資料是由熟練的攻擊者精心構造的。