申明: 本站飛宇網 https://feiyetopro.blogspot.com/。自網路收集整理之書籍、文章、影音僅供預覽交流學習研究,其[書籍、文章、影音]情節內容, 評論屬其個人行為, 與本網站無關。版權歸原作者和出版社所有,請在下載 24 小時內刪除,不得用作商業用途;如果您喜歡其作品,請支持訂閱購買[正版]。謝謝!
第 10 章 文件和異常
至此,你掌握了編寫組織有序而易於使用的程式所需的基本技能,該考慮讓程式目標更明確、用途更大了。在本章中,你將學習處理檔,讓程式能夠快速地分析大量的資料;你將學習錯誤處理,避免程式在面對意外情形時崩潰;你將學習異常 ,它們是Python創建的特殊物件,用於管理程式運行時出現的錯誤;你還將學習模組json ,它讓你能夠保存使用者資料,以免在程式停止運行後丟失。
學習處理檔和保存資料可讓你的程式使用起來更容易:使用者將能夠選擇輸入什麼樣的資料,以及在什麼時候輸入;使用者使用你的程式做一些工作後,可將程式關閉,以後再接著往下做。學習處理異常可幫助你應對檔不存在的情形,以及處理其他可能導致程式崩潰的問題。這讓你的程式在面對錯誤的資料時更健壯——不管這些錯誤資料來源自無意的錯誤,還是源自破壞程式的惡意企圖。你在本章學習的技能可提高程式的適用性、可用性和穩定性。
10.1 從檔中讀取資料
文字檔可存儲的資料量多得難以置信:天氣資料、交通資料、社會經濟資料、文學作品等。每當需要分析或修改存儲在檔中的資訊時,讀取檔都很有用,對資料分析應用程式來說尤其如此。例如,你可以編寫一個這樣的程式:讀取一個文字檔的內容,重新設置這些資料的格式並將其寫入檔,讓流覽器能夠顯示這些內容。
要使用文字檔中的資訊,首先需要將資訊讀取到記憶體中。為此,你可以一次性讀取檔的全部內容,也可以以每次一行的方式逐步讀取。
10.1.1 讀取整個文件
要讀取檔,需要一個包含幾行文本的檔。下面首先來創建一個檔,它包含精確到小數點後30位的圓周率值,且在小數點後每10位處都換行:
pi_digits.txt
3.1415926535
8979323846
2643383279
要動手嘗試後續示例,可在編輯器中輸入這些資料行,再將檔保存為pi_digits.txt,也可從本書的配套網站(https://www.nostarch.com/pythoncrashcourse/ )下載該文件。然後,將該檔保存到本章程式所在的目錄中。
下面的程式打開並讀取這個檔,再將其內容顯示到螢幕上:
file_reader.py
with
open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)
在這個程式中,第1行代碼做了大量的工作。我們先來看看函數open() 。要以任何方式使用檔——哪怕僅僅是列印其內容,都得先打開 檔,這樣才能訪問它。函數open() 接受一個參數:要打開的檔的名稱。Python在當前執行的檔所在的目錄中查找指定的檔。在這個示例中,當前運行的是file_reader.py,因此Python在file_reader.py所在的目錄中查找pi_digits.txt。函數open() 返回一個表示檔的物件。在這裡,open('pi_digits.txt') 返回一個表示檔pi_digits.txt 的對象;Python將這個物件存儲在我們將在後面使用的變數中。
關鍵字with 在不再需要訪問檔後將其關閉。在這個程式中,注意到我們調用了open() ,但沒有調用close() ;你也可以調用open() 和close() 來打開和關閉檔,但這樣做時,如果程式存在bug,導致close() 語句未執行,檔將不會關閉。這看似微不足道,但未妥善地關閉檔可能會導致資料丟失或受損。如果在程式中過早地調用close() ,你會發現需要使用檔時它已關閉 (無法訪問),這會導致更多的錯誤。並非在任何情況下都能輕鬆確定關閉檔的恰當時機,但通過使用前面所示的結構,可讓Python去確定:你只管打開檔,並在需要時使用它,Python自會在合適的時候自動將其關閉。
有了表示pi_digits.txt的檔物件後,我們使用方法read() (前述程式的第2行)讀取這個檔的全部內容,並將其作為一個長長的字串存儲在變數contents 中。這樣,通過列印contents 的值,就可將這個文字檔的全部內容顯示出來:
3.1415926535
8979323846
2643383279
相比於原始檔,該輸出唯一不同的地方是末尾多了一個空行。為何會多出這個空行呢?因為read() 到達檔末尾時返回一個空字串,而將這個空字串顯示出來時就是一個空行。要刪除多出來的空行,可在print 語句中使用rstrip() :
with
open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents.rstrip())
本書前面說過,Python方法rstrip() 刪除(剝除)字串末尾的空白。現在,輸出與原始檔的內容完全相同:
3.1415926535
8979323846
2643383279
10.1.2 檔路徑
當你將類似pi_digits.txt這樣的簡單檔案名傳遞給函數open() 時,Python將在當前執行的檔(即.py程式檔)所在的目錄中查找檔。
根據你組織檔的方式,有時可能要打開不在程式檔所屬目錄中的檔。例如,你可能將程式檔存儲在了資料夾python_work中,而在資料夾python_work中,有一個名為text_files的資料夾,用於存儲程式檔操作的文字檔。雖然資料夾text_files包含在資料夾python_work中,但僅向open() 傳遞位於該資料夾中的檔的名稱也不可行,因為Python只在資料夾python_work中查找,而不會在其子資料夾text_files中查找。要讓Python打開不與程式檔位於同一個目錄中的檔,需要提供檔路徑 ,它讓Python到系統的特定位置去查找。
由於資料夾text_files位於資料夾python_work中,因此可使用相對檔路 徑來打開該資料夾中的文件。相對檔路徑讓Python到指定的位置去查找,而該位置是相對於當前運行的程式所在目錄的。在Linux和OS X中,你可以這樣編寫代碼:
with
open('text_files/filename.txt') as file_object:
這行代碼讓Python到資料夾python_work下的資料夾text_files中去查找指定的.txt文件。在Windows系統中,在檔路徑中使用反斜線(\ )而不是斜杠(/ ):
with
open('text_files\filename.txt') as file_object:
你還可以將檔在電腦中的準確位置告訴Python,這樣就不用關心當前運行的程式存儲在什麼地方了。這稱為絕對檔路徑 。在相對路徑行不通時,可使用絕對路徑。例如,如果text_files並不在資料夾python_work中,而在資料夾other_files中,則向open() 傳遞路徑'text_files/ filename.txt' 行不通,因為Python只在資料夾python_work中查找該位置。為明確地指出你希望Python到哪裡去查找,你需要提供完整的路徑。
絕對路徑通常比相對路徑更長,因此將其存儲在一個變數中,再將該變數傳遞給open() 會有所幫助。在Linux和OS X中,絕對路徑類似於下面這樣:
file_path
= '/home/ehmatthes/other_files/text_files/filename.txt'
with
open(file_path) as file_object:
而在Windows系統中,它們類似於下面這樣:
file_path
= 'C:\Users\ehmatthes\other_files\text_files\filename.txt'
with
open(file_path) as file_object:
通過使用絕對路徑,可讀取系統任何地方的檔。就目前而言,最簡單的做法是,要麼將資料檔案存儲在程式檔所在的目錄,要麼將其存儲在程式檔所在目錄下的一個資料夾(如text_files)中。
注意 Windows系統有時能夠正確地解讀檔路徑中的斜杠。如果你使用的是Windows系統,且結果不符合預期,請確保在檔路徑中使用的是反斜線。
10.1.3 逐行讀取
讀取檔時,常常需要檢查其中的每一行:你可能要在檔中查找特定的資訊,或者要以某種方式修改檔中的文本。例如,你可能要遍歷一個包含天氣資料的檔,並使用天氣描述中包含字樣sunny的行。在新聞報導中,你可能會查找包含標籤<headline> 的行,並按特定的格式設置它。
要以每次一行的方式檢查檔,可對檔物件使用for 迴圈:
file_reader.py
❶ filename
= 'pi_digits.txt'
❷ with
open(filename) as file_object:
❸ for line in file_object:
print(line)
在❶處,我們將要讀取的檔的名稱存儲在變數filename 中,這是使用檔時一種常見的做法。由於變數filename 表示的並非實際檔——它只是一個讓Python知道到哪裡去查找檔的字串,因此可輕鬆地將'pi_digits.txt' 替換為你要使用的另一個檔的名稱。調用open() 後,將一個表示檔及其內容的物件存儲到了變數file_object 中(見❷)。這裡也使用了關鍵字with ,讓Python負責妥善地打開和關閉文件。為查看檔的內容,我們通過對檔物件執行迴圈來遍歷檔中的每一行(見❸)。
我們列印每一行時,發現空白行更多了:
3.1415926535
8979323846
2643383279
為何會出現這些空白行呢?因為在這個檔中,每行的末尾都有一個看不見的分行符號,而print 語句也會加上一個分行符號,因此每行末尾都有兩個分行符號:一個來自檔,另一個來自print 語句。要消除這些多餘的空白行,可在print 語句中使用rstrip() :
filename
= 'pi_digits.txt'
with
open(filename) as file_object:
for line in file_object:
print(line.rstrip())
現在,輸出又與檔內容完全相同了:
3.1415926535
8979323846
2643383279
10.1.4 創建一個包含檔各行內容的清單
使用關鍵字with 時,open() 返回的檔物件只在with 代碼塊內可用。如果要在with 代碼塊外訪問檔的內容,可在with 代碼塊內將檔的各行存儲在一個清單中,並在with 代碼塊外使用該列表:你可以立即處理檔的各個部分,也可推遲到程式後面再處理。
下面的示例在with 代碼塊中將文件pi_digits.txt的各行存儲在一個清單中,再在with 代碼塊外列印它們:
filename = 'pi_digits.txt'
with open(filename) as file_object:
❶ lines = file_object.readlines()
❷ for line
in lines:
print(line.rstrip())
❶處的方法readlines() 從檔中讀取每一行,並將其存儲在一個清單中;接下來,該清單被存儲到變數lines 中;在with 代碼塊外,我們依然可以使用這個變數。在❷處,我們使用一個簡單的for 迴圈來列印lines 中的各行。由於清單lines 的每個元素都對應于檔中的一行,因此輸出與檔內容完全一致。
10.1.5 使用檔的內容
將檔讀取到記憶體中後,就可以以任何方式使用這些資料了。下面以簡單的方式使用圓周率的值。首先,我們將創建一個字串,它包含檔中存儲的所有數位,且沒有任何空格:
pi_string.py
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
❶ pi_string
= ''
❷ for line
in lines:
pi_string += line.rstrip()
❸
print(pi_string)
print(len(pi_string))
就像前一個示例一樣,我們首先打開檔,並將其中的所有行都存儲在一個清單中。在❶處,我們創建了一個變數——pi_string ,用於存儲圓周率的值。接下來,我們使用一個迴圈將各行都加入pi_string ,並刪除每行末尾的分行符號(見❷)。在❸處,我們列印這個字串及其長度:
3.1415926535 8979323846
2643383279
36
在變數pi_string 存儲的字串中,包含原來位於每行左邊的空格,為刪除這些空格,可使用strip() 而不是rstrip() :
filename
= 'pi_30_digits.txt'
with
open(filename) as file_object:
lines = file_object.readlines()
pi_string
= ''
for
line in lines:
pi_string
+= line.strip()
print(pi_string)
print(len(pi_string))
這樣,我們就獲得了一個這樣的字串:它包含精確到30位小數的圓周率值。這個字串長32字元,因為它還包含整數部分的3和小數點:
3.141592653589793238462643383279
32
注意 讀取文字檔時,Python將其中的所有文本都解讀為字串。如果你讀取的是數位,並要將其作為數值使用,就必須使用函數int() 將其轉換為整數,或使用函數float() 將其轉換為浮點數。
10.1.6 包含一百萬位的大型檔
前面我們分析的都是一個只有三行的文字檔,但這些代碼示例也可處理大得多的檔。如果我們有一個文字檔,其中包含精確到小數點後1 000 000位而不是30位的圓周率值,也可創建一個包含所有這些數位的字串。為此,我們無需對前面的程式做任何修改,只需將這個檔傳遞給它即可。在這裡,我們只列印到小數點後50位元,以免終端為顯示全部1 000 000位而不斷地翻滾:
pi_string.py
filename
= 'pi_million_digits.txt'
with open(filename)
as file_object:
lines = file_object.readlines()
pi_string
= ''
for
line in lines:
pi_string += line.strip()
print(pi_string[:52]
+ "...")
print(len(pi_string))
輸出表明,我們創建的字串確實包含精確到小數點後1 000 000位的圓周率值:
3.14159265358979323846264338327950288419716939937510...
1000002
對於你可處理的資料量,Python沒有任何限制;只要系統的記憶體足夠多,你想處理多少資料都可以。
注意 要運行這個程式(以及後面的眾多示例),你需要從https://www.nostarch.com/pythoncra-shcourse/ 下載相關的資源。
10.1.7 圓周率值中包含你的生日嗎
我一直想知道自己的生日是否包含在圓周率值中。下面來擴展剛才編寫的程式,以確定某個人的生日是否包含在圓周率值的前1 000 000位中。為此,可將生日表示為一個由數位組成的字串,再檢查這個字串是否包含在pi_string 中:
filename = 'pi_million_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.rstrip()
❶ birthday
= input("Enter your birthday, in the form mmddyy: ")
❷ if
birthday in pi_string:
print("Your birthday appears in the
first million digits of pi!")
else:
print("Your birthday does not appear
in the first million digits of pi.")
在❶處,我們提示用戶輸入其生日,在接下來的❷處,我們檢查這個字串是否包含在pi_string 中。運行一下這個程式:
Enter
your birthdate, in the form mmddyy: 120372
Your
birthday appears in the first million digits of pi!
我的生日確實出現在了圓周率值中!讀取檔的內容後,就可以以你能想到的任何方式對其進行分析。
動手試一試
10-1 Python學習筆記 :在文字編輯器中新建一個檔,寫幾句話來總結一下你至此學到的Python知識,其中每一行都以“In Python you can”打頭。將這個檔命名為learning_python.txt,並將其存儲到為完成本章練習而編寫的程式所在的目錄中。編寫一個程式,它讀取這個檔,並將你所寫的內容列印三次:第一次列印時讀取整個檔;第二次列印時遍歷檔對象;第三次列印時將各行存儲在一個清單中,再在with 代碼塊外列印它們。
10-2 C語言學習筆記 :可使用方法replace() 將字串中的特定單詞都替換為另一個單詞。下面是一個簡單的示例,演示了如何將句子中的'dog' 替換為'cat' :
>>>
message = "I really like dogs."
>>>
message.replace('dog', 'cat')
'I
really like cats.'
讀取你剛創建的文件learning_python.txt中的每一行,將其中的Python都替換為另一門語言的名稱,如C。將修改後的各行都列印到螢幕上。
10.2 寫入文件
保存資料的最簡單的方式之一是將其寫入到檔中。通過將輸出寫入檔,即便關閉包含程式輸出的終端視窗,這些輸出也依然存在:你可以在程式結束運行後查看這些輸出,可與別人分享輸出檔,還可編寫程式來將這些輸出讀取到記憶體中並進行處理。
10.2.1 寫入空文件
要將文本寫入檔,你在調用open() 時需要提供另一個實參,告訴Python你要寫入打開的文件。為明白其中的工作原理,我們來將一條簡單的消息存儲到檔中,而不是將其列印到螢幕上:
write_message.py
filename = 'programming.txt'
❶ with
open(filename, 'w') as file_object:
❷ file_object.write("I love
programming.")
在這個示例中,調用open() 時提供了兩個實參(見❶)。第一個實參也是要打開的檔的名稱;第二個實參('w' )告訴Python,我們要以寫入模式 打開這個檔。打開檔時,可指定讀取模式 ('r' )、寫入模式 ('w' )、附加模式 ('a' )或讓你能夠讀取和寫入檔的模式('r+' )。如果你省略了模式實參,Python將以預設的唯讀模式打開檔。
如果你要寫入的檔不存在,函數open() 將自動創建它。然而,以寫入('w' )模式打開檔時千萬要小心,因為如果指定的檔已經存在,Python將在返回檔物件前清空該檔。
在❷處,我們使用檔物件的方法write() 將一個字串寫入檔。這個程式沒有終端輸出,但如果你打開檔programming.txt,將看到其中包含如下一行內容:
programming.txt
I
love programming.
相比於你的電腦中的其他檔,這個檔沒有什麼不同。你可以打開它、在其中輸入新文本、複製其內容、將內容粘貼到其中等。
注意 Python只能將字串寫入文字檔。要將數值資料存儲到文字檔中,必須先使用函數str() 將其轉換為字串格式。
10.2.2 寫入多行
函數write() 不會在你寫入的文本末尾添加分行符號,因此如果你寫入多行時沒有指定分行符號,檔看起來可能不是你希望的那樣:
filename
= 'programming.txt'
with
open(filename, 'w') as file_object:
file_object.write("I love
programming.")
file_object.write("I love creating new
games.")
如果你打開programming.txt,將發現兩行內容擠在一起:
I
love programming.I love creating new games.
要讓每個字串都單獨占一行,需要在write() 語句中包含分行符號:
filename
= 'programming.txt'
with open(filename,
'w') as file_object:
file_object.write("I love
programming.\n")
file_object.write("I love creating new
games.\n")
現在,輸出出現在不同行中:
I
love programming.
I
love creating new games.
像顯示到終端的輸出一樣,還可以使用空格、定位字元和空行來設置這些輸出的格式。
10.2.3 附加到檔
如果你要給檔添加內容,而不是覆蓋原有的內容,可以附加模式 打開檔。你以附加模式打開檔時,Python不會在返回檔物件前清空檔,而你寫入到文件的行都將添加到文件末尾。如果指定的檔不存在,Python將為你創建一個空檔。
下面來修改write_message.py,在既有文件programming.txt中再添加一些你酷愛程式設計的原因:
write_message.py
filename = 'programming.txt'
❶ with
open(filename, 'a') as file_object:
❷ file_object.write("I also love
finding meaning in large datasets.\n")
file_object.write("I love creating
apps that can run in a browser.\n")
在❶處,我們打開檔時指定了實參'a' ,以便將內容附加到檔末尾,而不是覆蓋檔原來的內容。在❷處,我們又寫入了兩行,它們被添加到檔programming.txt末尾:
programming.txt
I
love programming.
I
love creating new games.
I
also love finding meaning in large datasets.
I
love creating apps that can run in a browser.
最終的結果是,檔原來的內容還在,它們後面是我們剛添加的內容。
動手試一試
10-3 訪客 :編寫一個程式,提示使用者輸入其名字;用戶作出回應後,將其名字寫入到檔guest.txt中。
10-4 訪客名單 :編寫一個while 迴圈,提示用戶輸入其名字。使用者輸入其名字後,在螢幕上列印一句問候語,並將一條訪問記錄添加到檔guest_book.txt中。確保這個檔中的每條記錄都獨佔一行。
10-5 關於程式設計的調查 :編寫一個while 迴圈,詢問用戶為何喜歡程式設計。每當用戶輸入一個原因後,都將其添加到一個存儲所有原因的檔中。
10.3 異常
Python使用被稱為異常 的特殊物件來管理程式執行期間發生的錯誤。每當發生讓Python不知所措的錯誤時,它都會創建一個異常物件。如果你編寫了處理該異常的代碼,程式將繼續運行;如果你未對異常進行處理,程式將停止,並顯示一個traceback,其中包含有關異常的報告。
異常是使用try-except 代碼塊處理的。try-except 代碼塊讓Python執行指定的操作,同時告訴Python發生異常時怎麼辦。使用了try-except 代碼塊時,即便出現異常,程式也將繼續運行:顯示你編寫的友好的錯誤消息,而不是令使用者迷惑的traceback。
10.3.1 處理ZeroDivisionError 異常
下面來看一種導致Python引發異常的簡單錯誤。你可能知道不能將一個數字除以0,但我們還是讓Python這樣做吧:
division.py
print(5/0)
顯然,Python無法這樣做,因此你將看到一個traceback:
Traceback (most recent call last):
File "division.py", line 1, in
<module>
print(5/0)
❶
ZeroDivisionError: division by zero
在上述traceback中,❶處指出的錯誤ZeroDivisionError 是一個異常物件。Python無法按你的要求做時,就會創建這種物件。在這種情況下,Python將停止運行程式,並指出引發了哪種異常,而我們可根據這些資訊對程式進行修改。下面我們將告訴Python,發生這種錯誤時怎麼辦;這樣,如果再次發生這樣的錯誤,我們就有備無患了。
10.3.2 使用try-except 代碼塊
當你認為可能發生了錯誤時,可編寫一個try-except 代碼塊來處理可能引發的異常。你讓Python嘗試運行一些代碼,並告訴它如果這些代碼引發了指定的異常,該怎麼辦。
處理ZeroDivisionError 異常的try-except 代碼塊類似於下面這樣:
try:
print(5/0)
except
ZeroDivisionError:
print("You can't divide by
zero!")
我們將導致錯誤的代碼行print(5/0) 放在了一個try 代碼塊中。如果try 代碼塊中的代碼運行起來沒有問題,Python將跳過except 代碼塊;如果try 代碼塊中的代碼導致了錯誤,Python將查找這樣的except 代碼塊,並運行其中的代碼,即其中指定的錯誤與引發的錯誤相同。
在這個示例中,try 代碼塊中的代碼引發了ZeroDivisionError 異常,因此Python指出了該如何解決問題的except 代碼塊,並運行其中的代碼。這樣,使用者看到的是一條友好的錯誤消息,而不是traceback:
You
can't divide by zero!
如果try-except 代碼塊後面還有其他代碼,程式將接著運行,因為已經告訴了Python如何處理這種錯誤。下面來看一個捕獲錯誤後程式將繼續運行的示例。
10.3.3 使用異常避免崩潰
發生錯誤時,如果程式還有工作沒有完成,妥善地處理錯誤就尤其重要。這種情況經常會出現在要求使用者提供輸入的程式中;如果程式能夠妥善地處理無效輸入,就能再提示用戶提供有效輸入,而不至於崩潰。
下面來創建一個隻執行除法運算的簡單計算器:
division.py
print("Give me two numbers, and I'll
divide them.")
print("Enter 'q' to quit.")
while
True:
❶ first_number = input("\nFirst number:
")
if first_number == 'q':
break
❷ second_number = input("Second number:
")
if second_number == 'q':
break
❸ answer = int(first_number) /
int(second_number)
print(answer)
在❶處,這個程式提示使用者輸入一個數位,並將其存儲到變數first_number 中;如果用戶輸入的不是表示退出的q,就再提示使用者輸入一個數位,並將其存儲到變數second_number 中(見❷)。接下來,我們計算這兩個數字的商(即answer ,見❸)。這個程式沒有採取任何處理錯誤的措施,因此讓它執行除數為0的除法運算時,它將崩潰:
Give
me two numbers, and I'll divide them.
Enter
'q' to quit.
First
number: 5
Second
number: 0
Traceback
(most recent call last):
File "division.py", line 9, in
<module>
answer = int(first_number) /
int(second_number)
ZeroDivisionError:
division by zero
程式崩潰可不好,但讓用戶看到traceback也不是好主意。不懂技術的用戶會被它們搞糊塗,而且如果用戶懷有惡意,他會通過traceback獲悉你不希望他知道的資訊。例如,他將知道你的程式檔的名稱,還將看到部分不能正確運行的代碼。有時候,訓練有素的攻擊者可根據這些資訊判斷出可對你的代碼發起什麼樣的攻擊。
10.3.4 else 代碼塊
通過將可能引發錯誤的代碼放在try-except 代碼塊中,可提高這個程式抵禦錯誤的能力。錯誤是執行除法運算的代碼行導致的,因此我們需要將它放到try-except 代碼塊中。這個示例還包含一個else 代碼塊;依賴于try 代碼塊成功執行的代碼都應放到else 代碼塊中:
print("Give me two numbers, and I'll
divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFirst
number: ")
if first_number == 'q':
break
second_number = input("Second
number: ")
❶ try:
answer = int(first_number) /
int(second_number)
❷ except ZeroDivisionError:
print("You can't divide by 0!")
❸ else:
print(answer)
我們讓Python嘗試執行try 代碼塊中的除法運算(見❶),這個代碼塊只包含可能導致錯誤的代碼。依賴于try 代碼塊成功執行的代碼都放在else 代碼塊中;在這個示例中,如果除法運算成功,我們就使用else 代碼塊來列印結果(見❸)。
except 代碼塊告訴Python,出現ZeroDivisionError 異常時該怎麼辦(見❷)。如果try 代碼塊因除零錯誤而失敗,我們就列印一條友好的消息,告訴使用者如何避免這種錯誤。程式將繼續運行,使用者根本看不到traceback:
Give
me two numbers, and I'll divide them.
Enter
'q' to quit.
First
number: 5
Second
number: 0
You
can't divide by 0!
First
number: 5
Second
number: 2
2.5
First
number: q
try-except-else 代碼塊的工作原理大致如下:Python嘗試執行try 代碼塊中的代碼;只有可能引發異常的代碼才需要放在try 語句中。有時候,有一些僅在try 代碼塊成功執行時才需要運行的代碼;這些代碼應放在else 代碼塊中。except 代碼塊告訴Python,如果它嘗試運行try 代碼塊中的代碼時引發了指定的異常,該怎麼辦。
通過預測可能發生錯誤的代碼,可編寫健壯的程式,它們即便面臨無效資料或缺少資源,也能繼續運行,從而能夠抵禦無意的使用者錯誤和惡意的攻擊。
10.3.5 處理FileNotFoundError 異常
使用檔時,一種常見的問題是找不到檔:你要查找的檔可能在其他地方、檔案名可能不正確或者這個檔根本就不存在。對於所有這些情形,都可使用try-except 代碼塊以直觀的方式進行處理。
我們來嘗試讀取一個不存在的檔。下麵的程式嘗試讀取檔alice.txt的內容,但我沒有將這個檔存儲在alice.py所在的目錄中:
alice.py
filename
= 'alice.txt'
with
open(filename) as f_obj:
contents = f_obj.read()
Python無法讀取不存在的檔,因此它引發一個異常:
Traceback
(most recent call last):
File "alice.py", line 3, in
<module>
with open(filename) as f_obj:
FileNotFoundError:
[Errno 2] No such file or directory: 'alice.txt'
在上述traceback中,最後一行報告了FileNotFoundError 異常,這是Python找不到要打開的檔時創建的異常。在這個示例中,這個錯誤是函數open() 導致的,因此要處理這個錯誤,必須將try 語句放在包含open() 的代碼行之前:
filename
= 'alice.txt'
try:
with open(filename) as f_obj:
contents
= f_obj.read()
except
FileNotFoundError:
msg = "Sorry, the file " +
filename + " does not exist."
print(msg)
在這個示例中,try 代碼塊引發FileNotFoundError 異常,因此Python找出與該錯誤匹配的except 代碼塊,並運行其中的代碼。最終的結果是顯示一條友好的錯誤消息,而不是traceback:
Sorry,
the file alice.txt does not exist.
如果檔不存在,這個程式什麼都不做,因此錯誤處理代碼的意義不大。下面來擴展這個示例,看看在你使用多個檔時,異常處理可提供什麼樣的幫助。
10.3.6 分析文本
你可以分析包含整本書的文字檔。很多經典文學作品都是以簡單文字檔的方式提供的,因為它們不受版權限制。本節使用的文本來自專案Gutenberg(http://gutenberg.org/ ),這個項目提供了一系列不受版權限制的文學作品,如果你要在程式設計專案中使用文學文本,這是一個很不錯的資源。
下面來提取童話 Alice in Wonderland 的文本,並嘗試計算它包含多少個單詞。我們將使用方法split() ,它根據一個字串創建一個單字清單。下面是對只包含童話名"Alice
in Wonderland" 的字串調用方法split() 的結果:
>>>
title = "Alice in Wonderland"
>>>
title.split()
['Alice',
'in', 'Wonderland']
方法split() 以空格為分隔符號將字串分拆成多個部分,並將這些部分都存儲到一個清單中。結果是一個包含字串中所有單詞的清單,雖然有些單詞可能包含標點。為計算 Alice
in Wonderland 包含多少個單詞,我們將對整篇小說調用split() ,再計算得到的清單包含多少個元素,從而確定整篇童話大致包含多少個單詞:
filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " +
filename + " does not exist."
print(msg)
else:
# 計算檔大致包含多少個單詞
❶ words = contents.split()
❷ num_words = len(words)
❸ print("The file " + filename +
" has about " + str(num_words) + " words.")
我們把檔alice.txt移到了正確的目錄中,讓try 代碼塊能夠成功地執行。在❶處,我們對變數contents (它現在是一個長長的字串,包含童話 Alice
in Wonderland 的全部文本)調用方法split() ,以生成一個列表,其中包含這部童話中的所有單詞。當我們使用len() 來確定這個列表的長度時,就知道了原始字串大致包含多少個單詞(見❷)。在❸處,我們列印一條消息,指出檔包含多少個單詞。這些代碼都放在else 代碼塊中,因為僅當try 代碼塊成功執行時才執行它們。輸出指出了檔alice.txt包含多少個單詞:
The
file alice.txt has about 29461 words.
這個數字有點大,因為這裡使用的文字檔包含出版商提供的額外資訊,但與童話 Alice
in Wonderland 的長度相當一致。
10.3.7 使用多個檔
下面多分析幾本書。這樣做之前,我們先將這個程式的大部分代碼移到一個名為count_words() 的函數中,這樣對多本書進行分析時將更容易:
word_count.py
def count_words(filename):
❶ """計算一個檔大致包含多少個單詞"""
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " +
filename + " does not exist."
print(msg)
else:
# 計算檔大致包含多少個單詞
words = contents.split()
num_words = len(words)
print("The file " +
filename + " has about " + str(num_words) +
" words.")
filename = 'alice.txt'
count_words(filename)
這些代碼大都與原來一樣,我們只是將它們移到了函數count_words() 中,並增加了縮進量。修改程式的同時更新注釋是個不錯的習慣,因此我們將注釋改成了文檔字串,並稍微調整了一下措辭(見❶)。
現在可以編寫一個簡單的迴圈,計算要分析的任何文本包含多少個單詞了。為此,我們將要分析的檔的名稱存儲在一個清單中,然後對列表中的每個檔都調用count_words() 。我們將嘗試計算 Alice in Wonderland 、Siddhartha 、Moby Dick 和 Little
Women 分別包含多少個單詞,它們都不受版權限制。我故意沒有將siddhartha.txt放到word_count.py所在的目錄中,讓你能夠看到這個程式在檔不存在時處理得有多出色:
def
count_words(filename):
--snip--
filenames
= ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for
filename in filenames:
count_words(filename)
文件siddhartha.txt不存在,但這絲毫不影響這個程式處理其他檔:
The
file alice.txt has about 29461 words.
Sorry,
the file siddhartha.txt does not exist.
The
file moby_dick.txt has about 215136 words.
The
file little_women.txt has about 189079 words.
在這個示例中,使用try-except 代碼塊提供了兩個重要的優點:避免讓用戶看到traceback;讓程式能夠繼續分析能夠找到的其他檔。如果不捕獲因找不到siddhartha.txt而引發的FileNotFoundError 異常,用戶將看到完整的traceback,而程式將在嘗試分析 Siddhartha 後停止運行——根本不分析 Moby
Dick 和 Little
Women 。
10.3.8 失敗時一聲不吭
在前一個示例中,我們告訴用戶有一個檔找不到。但並非每次捕獲到異常時都需要告訴使用者,有時候你希望程式在發生異常時一聲不吭,就像什麼都沒有發生一樣繼續運行。要讓程式在失敗時一聲不吭,可像通常那樣編寫try 代碼塊,但在except 代碼塊中明確地告訴Python什麼都不要做。Python有一個pass 語句,可在代碼塊中使用它來讓Python什麼都不要做:
def count_words(filename):
"""計算一個檔大致包含多少個單詞"""
try:
--snip--
except FileNotFoundError:
❶ pass
else:
--snip--
filenames = ['alice.txt', 'siddhartha.txt',
'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)
相比於前一個程式,這個程式唯一不同的地方是❶處的pass 語句。現在,出現FileNotFoundError 異常時,將執行except 代碼塊中的代碼,但什麼都不會發生。這種錯誤發生時,不會出現traceback,也沒有任何輸出。用戶將看到存在的每個檔包含多少個單詞,但沒有任何跡象表明有一個檔未找到:
The
file alice.txt has about 29461 words.
The
file moby_dick.txt has about 215136 words.
The
file little_women.txt has about 189079 words.
pass 語句還充當了預留位置,它提醒你在程式的某個地方什麼都沒有做,並且以後也許要在這裡做些什麼。例如,在這個程式中,我們可能決定將找不到的檔的名稱寫入到檔missing_files.txt中。用戶看不到這個檔,但我們可以讀取這個檔,進而處理所有檔找不到的問題。
10.3.9 決定報告哪些錯誤
在什麼情況下該向使用者報告錯誤?在什麼情況下又應該在失敗時一聲不吭呢?如果用戶知道要分析哪些檔,他們可能希望在有檔沒有分析時出現一條消息,將其中的原因告訴他們。如果用戶只想看到結果,而並不知道要分析哪些檔,可能就無需在有些檔不存在時告知他們。向使用者顯示他不想看到的資訊可能會降低程式的可用性。Python的錯誤處理結構讓你能夠細緻地控制與使用者分享錯誤資訊的程度,要分享多少資訊由你決定。
編寫得很好且經過詳盡測試的代碼不容易出現內部錯誤,如語法或邏輯錯誤,但只要程式依賴於外部因素,如用戶輸入、存在指定的檔、有網路連結,就有可能出現異常。憑藉經驗可判斷該在程式的什麼地方包含異常處理塊,以及出現錯誤時該向使用者提供多少相關的資訊。
動手試一試
10-6 加法運算 :提示用戶提供數值輸入時,常出現的一個問題是,使用者提供的是文本而不是數位。在這種情況下,當你嘗試將輸入轉換為整數時,將引發TypeError 異常。編寫一個程式,提示使用者輸入兩個數字,再將它們相加並列印結果。在使用者輸入的任何一個值不是數字時都捕獲TypeError 異常,並列印一條友好的錯誤消息。對你編寫的程式進行測試:先輸入兩個數位,再輸入一些文本而不是數位。
10-7 加法計算器 :將你為完成練習10-6而編寫的代碼放在一個while 迴圈中,讓用戶犯錯(輸入的是文本而不是數位)後能夠繼續輸入數位。
10-8 貓和狗 :創建兩個文件cats.txt和dogs.txt,在第一個檔中至少存儲三隻貓的名字,在第二個檔中至少存儲三條狗的名字。編寫一個程式,嘗試讀取這些檔,並將其內容列印到螢幕上。將這些代碼放在一個try-except 代碼塊中,以便在檔不存在時捕獲FileNotFound 錯誤,並列印一條友好的消息。將其中一個檔移到另一個地方,並確認except 代碼塊中的代碼將正確地執行。
10-9 沉默的貓和狗 :修改你在練習10-8中編寫的except 代碼塊,讓程式在檔不存在時一言不發。
10-10 常見單詞 :訪問專案Gutenberg(http://gutenberg.org/ ),並找一些你想分析的圖書。下載這些作品的文字檔或將流覽器中的原始文本複製到文字檔中。
你可以使用方法count() 來確定特定的單詞或短語在字串中出現了多少次。例如,下面的代碼計算'row' 在一個字串中出現了多少次:
>>>
line = "Row, row, row your boat"
>>>
line.count('row')
2
>>>
line.lower().count('row')
3
請注意,通過使用lower() 將字串轉換為小寫,可捕捉要查找的單詞出現的所有次數,而不管其大小寫格式如何。
編寫一個程式,它讀取你在專案Gutenberg中獲取的檔,並計算單詞'the' 在每個檔中分別出現了多少次。
10.4 存儲資料
很多程式都要求使用者輸入某種資訊,如讓使用者存儲遊戲首選項或提供要視覺化的資料。不管專注的是什麼,程式都把使用者提供的資訊存儲在清單和字典等資料結構中。使用者關閉程式時,你幾乎總是要保存他們提供的資訊;一種簡單的方式是使用模組json 來存儲資料。
模組json 讓你能夠將簡單的Python資料結構轉儲到檔中,並在程式再次運行時載入該檔中的資料。你還可以使用json 在Python程式之間分享資料。更重要的是,JSON資料格式並非Python專用的,這讓你能夠將以JSON格式存儲的資料與使用其他程式設計語言的人分享。這是一種輕便格式,很有用,也易於學習。
注意 JSON(JavaScript Object Notation)格式最初是為JavaScript開發的,但隨後成了一種常見格式,被包括Python在內的眾多語言採用。
10.4.1 使用json.dump() 和json.load()
我們來編寫一個存儲一組數位的簡短程式,再編寫一個將這些數位讀取到記憶體中的程式。第一個程式將使用json.dump() 來存儲這組數位,而第二個程式將使用json.load() 。
函數json.dump() 接受兩個實參:要存儲的資料以及可用於存儲資料的檔物件。下面演示了如何使用json.dump() 來存儲數位清單:
number_writer.py
import json
numbers = [2, 3, 5, 7, 11, 13]
❶ filename
= 'numbers.json'
❷ with
open(filename, 'w') as f_obj:
❸ json.dump(numbers, f_obj)
我們先導入模組json ,再創建一個數字清單。在❶處,我們指定了要將該數位清單存儲到其中的檔的名稱。通常使用檔副檔名.json來指出檔存儲的資料為JSON格式。接下來,我們以寫入模式打開這個檔,讓json 能夠將資料寫入其中(見❷)。在❸處,我們使用函數json.dump() 將數位清單存儲到檔numbers.json中。
這個程式沒有輸出,但我們可以打開檔numbers.json,看看其內容。資料的存儲格式與Python中一樣:
[2,
3, 5, 7, 11, 13]
下面再編寫一個程式,使用json.load() 將這個清單讀取到記憶體中:
number_reader.py
import json
❶ filename
= 'numbers.json'
❷ with
open(filename) as f_obj:
❸ numbers = json.load(f_obj)
print(numbers)
在❶處,我們確保讀取的是前面寫入的檔。這次我們以讀取方式打開這個檔,因為Python只需讀取這個檔(見❷)。在❸處,我們使用函數json.load() 載入存儲在numbers.json中的資訊,並將其存儲到變數numbers 中。最後,我們列印恢復的數字清單,看看它是否與number_writer.py中創建的數位清單相同:
[2,
3, 5, 7, 11, 13]
這是一種在程式之間共用資料的簡單方式。
10.4.2 保存和讀取使用者生成的資料
對於使用者生成的資料,使用json 保存它們大有裨益,因為如果不以某種方式進行存儲,等程式停止運行時使用者的資訊將丟失。下面來看一個這樣的例子:使用者首次運行程式時被提示輸入自己的名字,這樣再次運行程式時就記住他了。
我們先來存儲使用者的名字:
remember_me.py
import json
❶ username
= input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
❷ json.dump(username, f_obj)
❸ print("We'll remember you when you
come back, " + username + "!")
在❶處,我們提示輸入用戶名,並將其存儲在一個變數中。接下來,我們調用json.dump() ,並將用戶名和一個檔物件傳遞給它,從而將用戶名存儲到檔中(見❷)。然後,我們列印一條消息,指出我們存儲了他輸入的資訊(見❸):
What
is your name? Eric
We'll
remember you when you come back, Eric!
現在再編寫一個程式,向其名字被存儲的使用者發出問候:
greet_user.py
import json
filename = 'username.json'
with open(filename) as f_obj:
❶ username = json.load(f_obj)
❷ print("Welcome back, " +
username + "!")
在❶處,我們使用json.load() 將存儲在username.json中的資訊讀取到變數username 中。恢復用戶名後,我們就可以歡迎用戶回來了(見❷):
Welcome
back, Eric!
我們需要將這兩個程式合併到一個程式(remember_me.py)中。這個程式運行時,我們將嘗試從檔username.json中獲取用戶名,因此我們首先編寫一個嘗試恢復用戶名的try 代碼塊。如果這個檔不存在,我們就在except 代碼塊中提示用戶輸入用戶名,並將其存儲在username.json中,以便程式再次運行時能夠獲取它:
remember_me.py
import json
# 如果以前存儲了用戶名,就載入它
# 否則,就提示使用者輸入用戶名並存儲它
filename = 'username.json'
try:
❶ with open(filename) as f_obj:
❷ username = json.load(f_obj)
❸ except
FileNotFoundError:
❹ username = input("What is your name?
")
❺ with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when
you come back, " + username + "!")
else:
print("Welcome back, " +
username + "!")
這裡沒有任何新代碼,只是將前兩個示例的代碼合併到了一個程式中。在❶處,我們嘗試打開檔username.json。如果這個檔存在,就將其中的用戶名讀取到記憶體中(見❷),再執行else 代碼塊,即列印一條歡迎使用者回來的消息。使用者首次運行這個程式時,檔username.json不存在,將引發FileNotFoundError 異常(見❸),因此Python將執行except 代碼塊:提示用戶輸入其用戶名(見❹),再使用json.dump() 存儲該用戶名,並列印一句問候語(見❺)。
無論執行的是except 代碼塊還是else 代碼塊,都將顯示用戶名和合適的問候語。如果這個程式是首次運行,輸出將如下:
What
is your name? Eric
We'll
remember you when you come back, Eric!
否則,輸出將如下:
Welcome
back, Eric!
這是程式之前至少運行了一次時的輸出。
10.4.3 重構
你經常會遇到這樣的情況:代碼能夠正確地運行,但可做進一步的改進——將代碼劃分為一系列完成具體工作的函數。這樣的過程被稱為重構 。重構讓代碼更清晰、更易於理解、更容易擴展。
要重構remember_me.py,可將其大部分邏輯放到一個或多個函數中。remember_me.py的重點是問候用戶,因此我們將其所有代碼都放到一個名為greet_user() 的函數中:
remember_me.py
import json
def greet_user():
❶ """問候用戶,並指出其名字"""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
username = input("What is your
name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you
when you come back, " + username + "!")
else:
print("Welcome back, " +
username + "!")
greet_user()
考慮到現在使用了一個函數,我們刪除了注釋,轉而使用一個文檔字串來指出程式是做什麼的(見❶)。這個程式更清晰些,但函數greet_user() 所做的不僅僅是問候使用者,還在存儲了用戶名時獲取它,而在沒有存儲用戶名時提示使用者輸入一個。
下面來重構greet_user() ,讓它不執行這麼多工。為此,我們首先將獲取存儲的用戶名的代碼移到另一個函數中:
import json
def get_stored_username():
❶ """如果存儲了用戶名,就獲取它"""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
❷ return None
else:
return username
def greet_user():
"""問候用戶,並指出其名字"""
username = get_stored_username()
❸ if username:
print("Welcome back, " +
username + "!")
else:
username = input("What is your
name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you
when you come back, " + username + "!")
greet_user()
新增的函數get_stored_username() 目標明確,❶處的文檔字串指出了這一點。如果存儲了用戶名,這個函數就獲取並返回它;如果檔username.json不存在,這個函數就返回None (見❷)。這是一種不錯的做法:函數要麼返回預期的值,要麼返回None ;這讓我們能夠使用函數的返回值做簡單測試。在❸處,如果成功地獲取了用戶名,就列印一條歡迎使用者回來的消息,否則就提示使用者輸入用戶名。
我們還需將greet_user() 中的另一個代碼塊提取出來:將沒有存儲用戶名時提示使用者輸入的代碼放在一個獨立的函數中:
import
json
def
get_stored_username():
"""如果存儲了用戶名,就獲取它"""
--snip--
def
get_new_username():
"""提示用戶輸入用戶名"""
username = input("What is your name?
")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
return username
def
greet_user():
"""問候用戶,並指出其名字"""
username = get_stored_username()
if username:
print("Welcome back, " +
username + "!")
else:
username = get_new_username()
print("We'll remember you when you
come back, " + username + "!")
greet_user()
在remember_me.py的這個最終版本中,每個函數都執行單一而清晰的任務。我們調用greet_user() ,它列印一條合適的消息:要麼歡迎老用戶回來,要麼問候新用戶。為此,它首先調用get_stored_username() ,這個函數只負責獲取存儲的用戶名(如果存儲了的話),再在必要時調用get_new_username() ,這個函數只負責獲取並存儲新使用者的用戶名。要編寫出清晰而易於維護和擴展的代碼,這種劃分工作必不可少。
動手試一試
10-11 喜歡的數位 :編寫一個程式,提示使用者輸入他喜歡的數字,並使用json.dump() 將這個數位存儲到檔中。再編寫一個程式,從檔中讀取這個值,並列印消息“I know your
favorite number! It's _____.”。
10-12 記住喜歡的數位 :將練習10-11中的兩個程式合而為一。如果存儲了使用者喜歡的數位,就向使用者顯示它,否則提示使用者輸入他喜歡的數位並將其存儲到檔中。運行這個程式兩次,看看它是否像預期的那樣工作。
10-13 驗證用戶 :最後一個remember_me.py版本假設用戶要麼已輸入其用戶名,要麼是首次運行該程式。我們應修改這個程式,以應對這樣的情形:當前和最後一次運行該程式的使用者並非同一個人。
為此,在greet_user() 中列印歡迎使用者回來的消息前,先詢問他用戶名是否是對的。如果不對,就調用get_new_username() 讓用戶輸入正確的用戶名。
10.5 小結
在本章中,你學習了:如何使用檔;如何一次性讀取整個檔,以及如何以每次一行的方式讀取檔的內容;如何寫入檔,以及如何將文本附加到檔末尾;什麼是異常以及如何處理常式可能引發的異常;如何存儲Python資料結構,以保存使用者提供的資訊,避免使用者每次運行程式時都需要重新提供。
在第11章中,你將學習高效的代碼測試方式,這可説明你確定代碼正確無誤,以及發現擴展現有程式時可能引入的bug。

0 留言:
發佈留言