7. 輸入與輸出?

程序輸出有幾種顯示方式;數(shù)據(jù)既可以輸出供人閱讀的形式,也可以寫入文件備用。本章探討一些可用的方式。

7.1. 更復(fù)雜的輸出格式?

至此,我們已學(xué)習(xí)了兩種寫入值的方法:表達(dá)式語(yǔ)句print() 函數(shù)。第三種方法是使用文件對(duì)象的 write() 方法;標(biāo)準(zhǔn)輸出文件稱為 sys.stdout。詳見標(biāo)準(zhǔn)庫(kù)參考。

對(duì)輸出格式的控制不只是打印空格分隔的值,還需要更多方式。格式化輸出包括以下幾種方法。

  • 使用 格式化字符串字面值 ,要在字符串開頭的引號(hào)/三引號(hào)前添加 fF 。在這種字符串中,可以在 {} 字符之間輸入引用的變量,或字面值的 Python 表達(dá)式。

    >>>
    >>> year = 2016
    >>> event = 'Referendum'
    >>> f'Results of the {year} {event}'
    'Results of the 2016 Referendum'
    
  • 字符串的 str.format() 方法需要更多手動(dòng)操作。該方法也用 {} 標(biāo)記替換變量的位置,雖然這種方法支持詳細(xì)的格式化指令,但需要提供格式化信息。

    >>>
    >>> 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%'
    
  • 最后,還可以用字符串切片和合并操作完成字符串處理操作,創(chuàng)建任何排版布局。字符串類型還支持將字符串按給定列寬進(jìn)行填充,這些方法也很有用。

如果不需要花哨的輸出,只想快速顯示變量進(jìn)行調(diào)試,可以用 repr()str() 函數(shù)把值轉(zhuǎn)化為字符串。

str() 函數(shù)返回供人閱讀的值,repr() 則生成適于解釋器讀取的值(如果沒(méi)有等效的語(yǔ)法,則強(qiáng)制執(zhí)行 SyntaxError)。對(duì)于沒(méi)有支持供人閱讀展示結(jié)果的對(duì)象, str() 返回與 repr() 相同的值。一般情況下,數(shù)字、列表或字典等結(jié)構(gòu)的值,使用這兩個(gè)函數(shù)輸出的表現(xiàn)形式是一樣的。字符串有兩種不同的表現(xiàn)形式。

示例如下:

>>>
>>> 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 占位符,并用字典的值進(jìn)行替換,但對(duì)格式控制的支持比較有限。

7.1.1. 格式化字符串字面值?

格式化字符串字面值 (簡(jiǎn)稱為 f-字符串)在字符串前加前綴 fF,通過(guò) {expression} 表達(dá)式,把 Python 表達(dá)式的值添加到字符串內(nèi)。

格式說(shuō)明符是可選的,寫在表達(dá)式后面,可以更好地控制格式化值的方式。下例將 pi 舍入到小數(shù)點(diǎn)后三位:

>>>
>>> import math
>>> print(f'The value of pi is approximately {math.pi:.3f}.')
The value of pi is approximately 3.142.

':' 后傳遞整數(shù),為該字段設(shè)置最小字符寬度,常用于列對(duì)齊:

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

還有一些修飾符可以在格式化前轉(zhuǎn)換值。 '!a' 應(yīng)用 ascii() ,'!s' 應(yīng)用 str(),'!r' 應(yīng)用 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'.

格式規(guī)范參考詳見參考指南 格式規(guī)格迷你語(yǔ)言。

7.1.2. 字符串 format() 方法?

str.format() 方法的基本用法如下所示:

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

花括號(hào)及之內(nèi)的字符(稱為格式字段)被替換為傳遞給 str.format() 方法的對(duì)象?;ɡㄌ?hào)中的數(shù)字表示傳遞給 str.format() 方法的對(duì)象所在的位置。

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

str.format() 方法中使用關(guān)鍵字參數(shù)名引用值。

>>>
>>> print('This {food} is {adjective}.'.format(
...       food='spam', adjective='absolutely horrible'))
This spam is absolutely horrible.

位置參數(shù)和關(guān)鍵字參數(shù)可以任意組合:

>>>
>>> print('The story of {0}, {1}, and {other}.'.format('Bill', 'Manfred',
                                                       other='Georg'))
The story of Bill, Manfred, and Georg.

如果不想分拆較長(zhǎng)的格式字符串,最好按名稱引用變量進(jìn)行格式化,不要按位置。這項(xiàng)操作可以通過(guò)傳遞字典,并用方括號(hào) '[]' 訪問(wèn)鍵來(lái)完成。

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

也可以用 '**' 符號(hào),把 table 當(dāng)作傳遞的關(guān)鍵字參數(shù)。

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

與內(nèi)置函數(shù) vars() 結(jié)合使用時(shí),這種方式非常實(shí)用,可以返回包含所有局部變量的字典。

例如,下面的代碼生成一組整齊的列,包含給定整數(shù)及其平方與立方:

>>>
>>> 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() 進(jìn)行字符串格式化的完整概述詳見 格式字符串語(yǔ)法 。

7.1.3. 手動(dòng)格式化字符串?

下面是使用手動(dòng)格式化方式實(shí)現(xiàn)的同一個(gè)平方和立方的表:

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

(注意,每列之間的空格是通過(guò)使用 print() 添加的:它總在其參數(shù)間添加空格。)

字符串對(duì)象的 str.rjust() 方法通過(guò)在左側(cè)填充空格,對(duì)給定寬度字段中的字符串進(jìn)行右對(duì)齊。同類方法還有 str.ljust()str.center() 。這些方法不寫入任何內(nèi)容,只返回一個(gè)新字符串,如果輸入的字符串太長(zhǎng),它們不會(huì)截?cái)嘧址?,而是原樣返回;雖然這種方式會(huì)弄亂列布局,但也比另一種方法好,后者在顯示值時(shí)可能不準(zhǔn)確(如果真的想截?cái)嘧址?,可以使?x.ljust(n)[:n] 這樣的切片操作 。)

另一種方法是 str.zfill() ,該方法在數(shù)字字符串左邊填充零,且能識(shí)別正負(fù)號(hào):

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

7.1.4. 舊式字符串格式化方法?

% 運(yùn)算符(求余符)也可用于字符串格式化。給定 'string' % values,則 string 中的 % 實(shí)例會(huì)以零個(gè)或多個(gè) values 元素替換。此操作被稱為字符串插值。例如:

>>>
>>> import math
>>> print('The value of pi is approximately %5.3f.' % math.pi)
The value of pi is approximately 3.142.

printf 風(fēng)格的字符串格式化 小節(jié)介紹更多相關(guān)內(nèi)容。

7.2. 讀寫文件?

open() returns a file object, and is most commonly used with two positional arguments and one keyword argument: open(filename, mode, encoding=None)

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

第一個(gè)實(shí)參是文件名字符串。第二個(gè)實(shí)參是包含描述文件使用方式字符的字符串。mode 的值包括 'r' ,表示文件只能讀取;'w' 表示只能寫入(現(xiàn)有同名文件會(huì)被覆蓋);'a' 表示打開文件并追加內(nèi)容,任何寫入的數(shù)據(jù)會(huì)自動(dòng)添加到文件末尾。'r+' 表示打開文件進(jìn)行讀寫。mode 實(shí)參是可選的,省略時(shí)的默認(rèn)值為 'r'

Normally, files are opened in text mode, that means, you read and write strings from and to the file, which are encoded in a specific encoding. If encoding is not specified, the default is platform dependent (see open()). Because UTF-8 is the modern de-facto standard, encoding="utf-8" is recommended unless you know that you need to use a different encoding. Appending a 'b' to the mode opens the file in binary mode. Binary mode data is read and written as bytes objects. You can not specify encoding when opening file in binary mode.

在文本模式下讀取文件時(shí),默認(rèn)把平臺(tái)特定的行結(jié)束符(Unix 上為 \n, Windows 上為 \r\n)轉(zhuǎn)換為 \n。在文本模式下寫入數(shù)據(jù)時(shí),默認(rèn)把 \n 轉(zhuǎn)換回平臺(tái)特定結(jié)束符。這種操作方式在后臺(tái)修改文件數(shù)據(jù)對(duì)文本文件來(lái)說(shuō)沒(méi)有問(wèn)題,但會(huì)破壞 JPEGEXE 等二進(jìn)制文件中的數(shù)據(jù)。注意,在讀寫此類文件時(shí),一定要使用二進(jìn)制模式。

在處理文件對(duì)象時(shí),最好使用 with 關(guān)鍵字。優(yōu)點(diǎn)是,子句體結(jié)束后,文件會(huì)正確關(guān)閉,即便觸發(fā)異常也可以。而且,使用 with 相比等效的 try-finally 代碼塊要簡(jiǎn)短得多:

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

如果沒(méi)有使用 with 關(guān)鍵字,則應(yīng)調(diào)用 f.close() 關(guān)閉文件,即可釋放文件占用的系統(tǒng)資源。

警告

調(diào)用 f.write() 時(shí),未使用 with 關(guān)鍵字,或未調(diào)用 f.close(),即使程序正常退出,也**可能** 導(dǎo)致 f.write() 的參數(shù)沒(méi)有完全寫入磁盤。

通過(guò) with 語(yǔ)句,或調(diào)用 f.close() 關(guān)閉文件對(duì)象后,再次使用該文件對(duì)象將會(huì)失敗。

>>>
>>> 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. 文件對(duì)象的方法?

本節(jié)下文中的例子假定已創(chuàng)建 f 文件對(duì)象。

f.read(size) 可用于讀取文件內(nèi)容,它會(huì)讀取一些數(shù)據(jù),并返回字符串(文本模式),或字節(jié)串對(duì)象(在二進(jìn)制模式下)。 size 是可選的數(shù)值參數(shù)。省略 sizesize 為負(fù)數(shù)時(shí),讀取并返回整個(gè)文件的內(nèi)容;文件大小是內(nèi)存的兩倍時(shí),會(huì)出現(xiàn)問(wèn)題。size 取其他值時(shí),讀取并返回最多 size 個(gè)字符(文本模式)或 size 個(gè)字節(jié)(二進(jìn)制模式)。如已到達(dá)文件末尾,f.read() 返回空字符串('')。

>>>
>>> f.read()
'This is the entire file.\n'
>>> f.read()
''

f.readline() 從文件中讀取單行數(shù)據(jù);字符串末尾保留換行符(\n),只有在文件不以換行符結(jié)尾時(shí),文件的最后一行才會(huì)省略換行符。這種方式讓返回值清晰明確;只要 f.readline() 返回空字符串,就表示已經(jīng)到達(dá)了文件末尾,空行使用 '\n' 表示,該字符串只包含一個(gè)換行符。

>>>
>>> f.readline()
'This is the first line of the file.\n'
>>> f.readline()
'Second line of the file\n'
>>> f.readline()
''

從文件中讀取多行時(shí),可以用循環(huán)遍歷整個(gè)文件對(duì)象。這種操作能高效利用內(nèi)存,快速,且代碼簡(jiǎn)單:

>>>
>>> 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 的內(nèi)容寫入文件,并返回寫入的字符數(shù)。

>>>
>>> f.write('This is a test\n')
15

寫入其他類型的對(duì)象前,要先把它們轉(zhuǎn)化為字符串(文本模式)或字節(jié)對(duì)象(二進(jìn)制模式):

>>>
>>> value = ('the answer', 42)
>>> s = str(value)  # convert the tuple to string
>>> f.write(s)
18

f.tell() 返回整數(shù),給出文件對(duì)象在文件中的當(dāng)前位置,表示為二進(jìn)制模式下時(shí)從文件開始的字節(jié)數(shù),以及文本模式下的意義不明的數(shù)字。

f.seek(offset, whence) 可以改變文件對(duì)象的位置。通過(guò)向參考點(diǎn)添加 offset 計(jì)算位置;參考點(diǎn)由 whence 參數(shù)指定。 whence 值為 0 時(shí),表示從文件開頭計(jì)算,1 表示使用當(dāng)前文件位置,2 表示使用文件末尾作為參考點(diǎn)。省略 whence 時(shí),其默認(rèn)值為 0,即使用文件開頭作為參考點(diǎn)。

>>>
>>> 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 時(shí)打開的文件)中,只允許相對(duì)于文件開頭搜索(使用 seek(0, 2) 搜索到文件末尾是個(gè)例外),唯一有效的 offset 值是能從 f.tell() 中返回的,或 0。其他 offset 值都會(huì)產(chǎn)生未定義的行為。

文件對(duì)象還支持 isatty()truncate() 等方法,但不常用;文件對(duì)象的完整指南詳見庫(kù)參考。

7.2.2. 使用 json 保存結(jié)構(gòu)化數(shù)據(jù)?

從文件寫入或讀取字符串很簡(jiǎn)單,數(shù)字則稍顯麻煩,因?yàn)?read() 方法只返回字符串,這些字符串必須傳遞給 int() 這樣的函數(shù),接受 '123' 這樣的字符串,并返回?cái)?shù)字值 123。保存嵌套列表、字典等復(fù)雜數(shù)據(jù)類型時(shí),手動(dòng)解析和序列化的操作非常復(fù)雜。

Python 支持 JSON (JavaScript Object Notation) 這種流行數(shù)據(jù)交換格式,用戶無(wú)需沒(méi)完沒(méi)了地編寫、調(diào)試代碼,才能把復(fù)雜的數(shù)據(jù)類型保存到文件。json 標(biāo)準(zhǔn)模塊采用 Python 數(shù)據(jù)層次結(jié)構(gòu),并將之轉(zhuǎn)換為字符串表示形式;這個(gè)過(guò)程稱為 serializing (序列化)。從字符串表示中重建數(shù)據(jù)稱為 deserializing (解序化)。在序列化和解序化之間,表示對(duì)象的字符串可能已經(jīng)存儲(chǔ)在文件或數(shù)據(jù)中,或通過(guò)網(wǎng)絡(luò)連接發(fā)送到遠(yuǎn)方 的機(jī)器。

備注

JSON 格式通常用于現(xiàn)代應(yīng)用程序的數(shù)據(jù)交換。程序員早已對(duì)它耳熟能詳,可謂是交互操作的不二之選。

只需一行簡(jiǎn)單的代碼即可查看某個(gè)對(duì)象的 JSON 字符串表現(xiàn)形式:

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

dumps() 函數(shù)還有一個(gè)變體, dump() ,它只將對(duì)象序列化為 text file 。因此,如果 ftext file 對(duì)象,可以這樣做:

json.dump(x, f)

To decode the object again, if f is a binary file or text file object which has been opened for reading:

x = json.load(f)

備注

JSON files must be encoded in UTF-8. Use encoding="utf-8" when opening JSON file as a text file for both of reading and writing.

這種簡(jiǎn)單的序列化技術(shù)可以處理列表和字典,但在 JSON 中序列化任意類的實(shí)例,則需要付出額外努力。json 模塊的參考包含對(duì)此的解釋。

參見

pickle - 封存模塊

JSON 不同,pickle 是一種允許對(duì)復(fù)雜 Python 對(duì)象進(jìn)行序列化的協(xié)議。因此,它為 Python 所特有,不能用于與其他語(yǔ)言編寫的應(yīng)用程序通信。默認(rèn)情況下它也是不安全的:如果解序化的數(shù)據(jù)是由手段高明的攻擊者精心設(shè)計(jì)的,這種不受信任來(lái)源的 pickle 數(shù)據(jù)可以執(zhí)行任意代碼。