IO/格式/編碼

April 8, 2022

在〈簡介模組〉看過,使用者輸入的命令列引數,會收集為字串清單指定給 sys.argv 參考,在程式執行的過程中,也可以使用 input 函式取得使用者輸入,input 可以指定提示文字,使用者輸入的文字會以字串傳回:

name = input('請輸入你的名稱:')
print('歡迎, ', name)

原始碼檔案編碼

預設 .py 檔案必須是 UTF-8 編碼,可如下載入指令稿直譯並執行:

> python welcome.py
請輸入你的名稱:良葛格
歡迎,  良葛格

就現代的程式開發而言,鼓勵使用 UTF-8 撰寫原始碼檔案,如果 .py 檔案是 UTF-8 以外的編碼,必須在第一行放置編碼聲明(encoding declaration)。例如若為 MS950 編碼的 .py 檔案:

# coding: MS950
name = input('請輸入你的名稱:')
print('歡迎, ', name)

# 開頭代表這是一行註解,# 之後不會被當成是程式碼的一部份,這是告知 python 原始碼編碼最簡單的方式,你可能會看過其他的編碼設定方式,例如:

# -*- coding: Big5 -*-

或者是:

# vim: set fileencoding=Big5 :

也許你還會看到更多其他的方式,這是因為實際上,python 直譯器只要在註解中看到 coding=<encoding name> 或者 coding: <encoding name> 出現就可以了,因此,就算在第一行撰寫 #orz coding=MS950、#XDcoding: MS950,也都可正確找出文字編碼設定。

這是為了因應各種編輯器的特性,有興趣的話,可以參考〈PEP 0263〉,當中有說明,python 直譯器會使用底下 ^[ \t\v]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+) 規則表示式(Regular expression)來擷取文字編碼設定。

到目前為止,輸出都是使用 print 函式,使用 help(print) 查詢,可以看到函式簽署:

print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

可以看到,除了指定值輸出,還可以使用 sep 指定輸出值間的分隔字元,預設值為一個空白,可以使用 end 指定輸出後最後一個字元,預設值是 '\n'。例如:

>>> print(1, 2, 3)
1 2 3
>>> print(1, 2, 3, sep='')
123
>>> print(1, 2, 3, sep='', end='\n\n')
123

>>>

預設的輸出是系統標準輸出,可以使用 file 指定至其他輸出目的地。例如以下會將指定的值輸出至 data.txt:

>>> print(1, 2, 3, file = open('data.txt', 'w'))
>>>

open 函式會開啟檔案,並傳回一個(_io.TextIOWrapper)實例,上例將之指定給 print 作為輸出目標。

字串格式化

你可以格式化字串,至 Python 3.10 為止,支援三種字串格式化,分別是舊式字串格式化、新式字串格式化與字串格式化實字。

舊式字串格式化是從 Python 2.x 就存在的字串格式化方式,在 Python 3.x 支援,僅僅是為了相容性,不建議使用,底下僅示範一些簡單的舊式格式化:

>>> text = '%d %.2f %s' % (1, 99.3, 'Justin')
>>> print(text)
1 99.30 Justin
>>> print('%d %.2f %s' % (1, 99.3, 'Justin'))
1 99.30 Justin
>>>

格式化字串時使用的 %d%f%s 等佔位符號與 C 語言類似,之後使用 % 接上一個 tuple,也就是範例中以 () 包括的實字表示部份。

在 Python 3.x 中,建議使用新的格式化方式。直接來看幾個範例:

>>> '{} 除以 {} 是 {}'.format(10, 3, 10 / 3)
'10 除以 3 是 3.3333333333333335'
>>> '{2} 除以 {1} 是 {0}'.format(10 / 3, 3, 10)
'10 除以 3 是 3.3333333333333335'
>>> '{n1} 除以 {n2} 是 {result}'.format(result = 10 / 3, n1 = 10, n2 = 3)
'10 除以 3 是 3.3333333333333335'
>>>

新的格式化方式中,佔位符號的部份使用 {},若當中沒有數字或名稱,字串的 format 方法就要依序指定對應的數值,若 {} 中有數字,例如 {1},就表示使用 format 方法中第二個引數,這是因為索引值從 0 開始。若 {} 中指定了名稱,例如 {n1},就表示使用 format 中的具名參數 n1 對應的值,在這種情況下,不用在意 n1n2resultformat 中的指定順序。

無論是 {0} 或是 {n} 這樣的方式,都可以指定型態,也可以指定欄位寬度與小數點個數,例如:

>>> '{0:d} 除以 {1:d} 是 {2:f}'.format(10, 3, 10 / 3)
'10 除以 3 是 3.333333'
>>> '{0:5d} 除以 {1:5d} 是 {2:10.2f}'.format(10, 3, 10 / 3)
'   10 除以     3 是       3.33'
>>> '{n1:5d} 除以 {n2:5d} 是 {r:.2f}'.format(n1 = 10, n2 = 3, r = 10 / 3)
'   10 除以     3 是 3.33'
>>> '{n1:<5d} 除以 {n2:<5d} 是 {r:.2f}'.format(n1 = 10, n2 = 3, r = 10 / 3)
'10    除以 3     是 3.33'
>>> '{n1:>5d} 除以 {n2:>5d} 是 {r:.2f}'.format(n1 = 10, n2 = 3, r = 10 / 3)
'   10 除以     3 是 3.33'
>>> '{n1:*^5d} 除以 {n2:!^5d} 是 {r:.2f}'.format(n1 = 10, n2 = 3, r = 10 / 3)
'*10** 除以 !!3!! 是 3.33'
>>>

在上面的範例中,< 用來指定向左靠齊,> 用來指定向右靠齊,若沒有指定,預設是向右靠齊;如果想在數字位數不足欄位寬度時,補上指定的字元,可以在 ^ 前面指定。

format 方法甚至可以進行簡單的運算,像是使用索引取得清單元素值,使用鍵(Key)名稱取得字典(之後文件會談到)對應的值,或者存取模組中的名稱,例如:

>>> names = ['Justin', 'Monica', 'Irene']
>>> 'All Names: {n[0]}, {n[1]}, {n[2]}'.format(n = names)
'All Names: Justin, Monica, Irene'
>>> passwords = {'Justin': 123456, 'Monica': 654321}
>>> 'The password of Justin is {passwds[Justin]}'.format(passwds = passwords)
'The password of Justin is 123456'
>>> import sys
>>> 'My platform is {pc.platform}'.format(pc = sys)
'My platform is win32'
>>>

從 Python 3.6 開始,若撰寫字串實字時以 fF 作為前置,就可以進行字串的格式化,又稱 f-strings,在三種字串格式化方案中,f-strings 的表達能力是最好的,在允許的情況下,建議使用 f-strings 來進行字串格式化。

f-strings 的 {} 間可以撰寫運算式,運算結果與其他字串結合後傳回。例如:

>>> name = 'Justin'
>>> f'Hello, {name}'
'Hello, Justin'
>>> f'1 + 2 = {1 + 2}'
'1 + 2 = 3'
>>> f'you want to show {{ and }}?'
'you want to show { and }?'
>>>

由於 {} 用來標示字串中要先進行運算的部份,若要在 f-strings 中表示 {},必須分別使用 {{}}

f-strings 的 {} 間可以撰寫運算式,因而像 if/else 運算式、函式呼叫等也都可以。例如:

>>> name = None
>>> f'Hello, {"Guest" if name == None else name}'
'Hello, Guest'
>>> name = 'Justin'
>>> f'Hello, {"Guest" if name == None else name}'
'Hello, Justin'
>>> f'"林"的 16 進位值 {ascii("林")}'
'"林"的 16 進位值 \'\\u6797\''
>>>

if/else 運算式寫在 f-strings 中顯然不易閱讀,這只是個示範,f-strings在使用上,應以易讀易寫為優先考量。

f-strings 的 {} 中若要指定欄位寬度與小數點個數,可以使用 :,例如,將新式字串格式化中的一個範例,改為使用 f-strings 來寫的話會是:

>>> '{0:5d} 除以 {1:5d} 是 {2:10.2f}'.format(10, 3, 10 / 3)
'   10 除以     3 是       3.33'
>>> n = 10
>>> m = 3
>>> f'{n:5d} 除以 {m:5d} 是 {n / m:10.2f}'
'   10 除以     3 是       3.33'
>>>

有時在建立日誌用的訊息時,會想要顯示運算式與值,在 Python 3.7 以前透過 f-strings,必須如下撰寫:

>>> level = 10
>>> f'level = {level}'
'level = 10'
>>> name = 'justin'
>>> f'name.upper() = {name.upper()}'
'name.upper() = JUSTIN'
>>>

顯然地,重複撰寫了運算式是個麻煩,Python 3.8 以後改進了這點,可以如下撰寫:

>>> level = 10
>>> f'{level=}'
'level=10'
>>> f'{level = }'
'level = 10'
>>> name = 'justin'
>>> f'{name.upper()=}'
"name.upper()='JUSTIN'"
>>> f'{name.upper() = :-^20}'
'name.upper() = -------JUSTIN-------'
>>> f'Hello, {"Guest" if name == None else name=}'
'Hello, "Guest" if name == None else name=\'justin\''
>>>

f-strings 使用 f'{expr=}' 格式時,會自動記錄 expr 實際的名稱,= 前後可以有空白,= 之後可以指定格式,expr 也可以是函式呼叫等合法的運算式,不過記得撰寫時,一切以可讀性為主。

如果某物件有固定的格式化方式,可以在定義類別時定義特殊方法 __format__,在格式化的場合,詳情可參考〈object.format〉。

open 函式

如果你要將資料寫入檔案或從檔案讀出,可以使用 open 函式:

open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

例如,若要讀取檔案:

name = input('請輸入檔名:')
file = open(name, 'r', encoding = 'UTF-8')
content = file.read()
print(content)
file.close()

read 方法會讀取全部的檔案內容,在不使用檔案時,要使用 close 將檔案關閉。如果要逐行讀取,可以使用 readline 方法。例如:

name = input('請輸入檔名:')
file = open(name, 'r', encoding = 'UTF-8')
while True:
    line = file.readline()
    if not line: break
    print(line, end='')
file.close()

如果資料讀取完畢,readline 會傳回空字串,這在布林判斷式中會是 false。另一個比較簡潔的方式是使用 for 迴圈。例如:

name = input('請輸入檔名:')
file = open(name, 'r', encoding = 'UTF-8')
for line in file.readlines():
    print(line, end='')
file.close()

readlines 方法會用一個字串陣列收集讀取的每一行,for 迴圈每次取出字串陣列中的一個字串元素,並使用 print 函式顯示。

其實更有效率的方式,是呼叫檔案物件的 next 方法,它每次會傳回下一行,並在沒有資料可讀取時丟出 StopIteration,而 for 迴圈會自動呼叫 next 方法,並在捕捉到 StopIteration 時離開迴圈,因此就可以如下撰寫:

name = input('請輸入檔名:')
for line in open(name, 'r', encoding = 'UTF-8'):
    print(line, end='')

這樣的寫法會自動關閉檔案,不過牽涉到更多技術細節,現階段你只要記得有這種用法,技術細節在之後的文件還會介紹。

如果要寫資料至檔案,使用 open 函式時要指定模式為 'w',可使用 write 方法進行資料寫入。例如:

name = input('請輸入檔名:')
file = open(name, 'w', encoding = 'UTF-8')
file.write('test')
file.close()

若需要了解更多 open 函式的細節,可以使用 help 進行查詢。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter