Python 3 Tutorial 第二堂(1)Unicode 支援、基本 I/O


在第一堂下課之前,我們完成了練習 1:哈囉!世界!,那麼那些程式碼中做了什麼?為了方便,把範例程式碼再貼過來一下:

filename = input('檔名:')

file = open(filename, 'r', encoding='UTF-8')
content = file.read()
file.close()

print(content)
print(content.encode('UTF-8'))                 # 這是什麼?
print(content.encode('UTF-8').decode('UTF-8')) # 這是什麼?

原始碼檔案的編碼

首先,程式一開始使用 input 函式指定提示訊息,這會讓程式停下來等待使用者的輸入。在提示訊息的部份使用了中文,在 Ubuntu 中這沒有問題,然而,若你試著在 Windows 環境中進行這個課程,第一個練習可能就會遇上問題:

Unicode 支援、基本 I/O

這是因為 Python 3.x 中,python 直譯器預期的 .py 編碼,預設是 UTF-8,而在 Ubuntu 15.10 中,預設採用的文字編碼也是 UTF-8,這時在 .py 檔案中撰寫中文,並不會有問題發生,然而,你知道在 Windows 中,使用記事本編輯文字檔案時,預設的文字編碼是什麼嗎?

有趣的是,有時我上課會問學員一個問題:「你用的原始碼文字編碼是什麼?」很多學員答不出來,很多人不知道自己作業系統中開個純文字檔編碼是什麼,不知道在整合開發環境(Integrated Development Environment, IDE)中開個原始碼編碼是什麼,當然也就不知道為什麼把 A 專案的原始碼放到 B 專案中程式碼會出現亂碼。

如果你連 UTF-8 是什麼都不知道,那建議你看看我寫的〈亂碼 1/2〉中這幾篇文件:

在 Windows 中使用記事本編輯文字檔案,預設的編碼是 MS950,在 Python 3.x 中,若 .py 檔案的編碼若不是 UTF-8,必須在檔案的一開頭作編碼宣告(Encoding declaration):

Unicode 支援、基本 I/O

編碼宣告是個魔法註解(Magic comment),在上頭的例子中,# coding=MS950 告訴 Python 直譯器,這個原始碼檔案是以 MS950 來編碼,如此就能正確地讀取 .py 檔案。

Python 的 Unicode 支援

Python 3.x 中,文字是 str 型態的實例,不過 str 代表的是 Unicode,下面這個程式在 Python 3.x 中執行的話:

text = '測試'
print(type(text)) # 顯示 "<class 'str'>"
print(len(text)) # 顯示 2

type 函式可用來得知資料的型態,而 len 會表示有兩個「字元」,這樣的結果代表著 Python 3.x 對 Unicode 有比較好的支援,反觀在 Python 2.x,程式中,文字雖是 str 的實例,然而卻是代表文字資料的位元組序列(Byte sequence)。例如在 Python 2.x 中,以下的程式會顯示 6,即使 '測試' 是兩個字元:

# coding=UTF-8
text = '測試'
print len(text) # 顯示 6

這是因為 '測試' 這兩個字元,使用 UTF-8 編碼的話,會使用六個位元組,在 Python 2.x 中,len 函式實際上是計算位元組序列的長度,而不是字元長度。

在 Python 2.x 中,如果想要用 Unicode 來代表文字,也就是想要用 unicode 型態來封裝文字,可以使用 Unicode 字面常量(Unicode literal) 來表示,也就是在文字前置一個 u 符號。例如:

# coding=UTF-8
text = u'測試'
print type(text) # 顯示 "<type 'unicode'>"
print len(text) # 顯示 2

Python 2.x 直譯器執行程式時,會使用 unicode 實例來代表文字資料,使用 len 取得一個 unicode 實例的長度時,它會告訴你有幾個字元。

為了讓 Python 3.x 與 2.x 之間有更好的相容性,在 Python 3.5 中,也可以使用 u 明確指定是一個 str 實例(記得,Python 3.x 中是代表 Unicode),而使用 b 的話,表示是一個 bytes 實例,也就是一個位元組序列。底下顯示了 Python 3.5 與 Python 2.7 的文字編碼差異:

Unicode 支援、基本 I/O

在 Python 3.x 中,如果想取得文字實際編碼後的位元組序列,可以使用 encode 方法指定編碼,這也會傳回一個 bytes 實例,如果有個 bytes 實例,可以使用 decode 指定編碼,傳回代表 Unicode 的 str 實例。例如下圖是在 Python 3.x 互動交談環境中的測試實例:

Unicode 支援、基本 I/O

如果想知道更多 Python 中有關文字編碼的細節,可以再參考〈Python 的編碼〉這篇文件。

基本 I/O

接下來看看有關基本 I/O 的部份,你可以使用 open 函式來開啟檔案,開啟時指定存取模式,'r' 表示讀取,'w' 表示寫入,由於在 Ubuntu 中,預設的文字編碼是 UTF-8,若你讀取的檔案中含有中文,最好是指定 encoding 參數,若不指定的話,就看系統是什麼編碼了,具體來說,就是以 locale.getpreferredencoding(False) 的傳回結果作為讀取時的編碼。

open 函式會傳回 _io.TextIOWrapper 實例,使用 read 方法可以讀取檔案內容,以 str 型態傳回,以下是個實際的讀取程式範例:

import sys
file = open(sys.argv[1], 'r')
content = file.read()
file.close()
print(content)

程式第一行匯入(import)了 sys 模組,sys.argv 是個 list,其中儲存了執行程式時的命令列引數(Command line arguments),索引 0 固定都是執行時的模組名稱,而後是跟隨著的引數,例如執行 python3.5 hello.py one two three 時,sys.argv[0] 就會是 'hello.py',其餘索引則是 'one''two''three'print() 在 Python 3.x 中是個函式,用來顯示指定的資料。不使用檔案時,記得使用 close 關閉檔案。

類似地,一個寫入檔案的程式範例如下,write 方法會將文字的位元組序列寫入至檔案中:

import sys
file = open(sys.argv[1], 'w')
file.write('test')
file.close()

如果要逐行讀取檔案呢?可以使用 open 傳回實例上的 readline 方法,例如逐行讀取一個文字檔案的所有內容,可以在 while 迴圈中進行:

import sys
file = open(sys.argv[1], 'r')
while True:
    line = file.readline()
    if not line: break
    print(line, end = '')
file.close()

如果讀不到東西了,那 readline 會傳回 '',在 if 判斷式中,'' 會被視為 False

while 後加上 : 表示區塊開始,Python 中使用縮排來決定區塊範圍。注意!你可以自行決定縮排字元,但是 Python 中縮排要一致,如果縮排想使用 Tab 字元,那所有原始碼就都要使用 Tab 字元縮排,如果要使用四個空白字元,那所有原始碼就得是四個空白字元來縮排,強制統一縮排,是 Python 的特色,也是 Python 的文化。

print() 函式顯示完指定的文字後,預設會加上換行字元,若想指定其他字元,可以在 end 參數上指定,在上例中,加上了 end = '',也就不會換行了。

可以使用 readlines 方法一次讀取所有檔案內容,這會傳回 list,每個索引處代表一行內容,一個程式範例是這樣的:

import sys
file = open(sys.argv[1], 'r')
for line in file.readlines():
    print(line, end = '')
file.close()

for in 語法可作用在 list 上,逐一取出 list 中的元素並指定給變數,對於 for line in file.readlines() 是這樣閱讀的:對於 file.readlines() 傳回的 list 中每個元素,將之指定給 line

實際上對於 Python 來說,讀取檔案最好的方式,就是不要去 read 它,這是什麼意思?是這樣的…

import sys
for line in open(sys.argv[1], 'r'):
    print(line, end = '')

這是 Python 的風格,也是 Python 的文化,這樣的寫法好處就是增加了可讀性,你不用自行使用 close 關閉檔案,在 open 傳回的實例被回收後,檔案就會關閉for line in open(sys.argv[1], 'r') 是這樣閱讀的:對於開啟檔案的每一行。除了可讀性外,這個語法還能讓讀取更有效率,不過現階段你不用管這些事,只要當這語法背後施了些魔法就好。